[{"content":"准备工作 工具介绍 软件 Keil5: 代码编译调试 CubeMX: 代码生成工具 VSCode: 代码编辑器 Modbus Poll \u0026amp;\u0026amp; Slave: 调试工具 硬件 STM32F407ZGT6: 普中麒麟F407开发板 两个串口转USB: 用于连接主机和从机 创建工程 CubeMX工程创建 选择芯片型号 设置时钟（外部晶振） 设置调试接口和基准时钟 配置串口（USART1~3） 开启FreeRTOS 创建3个任务 生成项目 Keil工程整合 将下载的Freemodbus源码复制到项目根目录，添加所需源文件：\nmodbus/functions/ 下所有文件 modbus/rtu/ 下所有文件 mb.c 和 mb_m.c 移植详解 port.c - 临界区实现 #include \u0026#34;FreeRTOS.h\u0026#34; #include \u0026#34;task.h\u0026#34; #include \u0026#34;semphr.h\u0026#34; void EnterCriticalSection(void) { taskENTER_CRITICAL(); } void ExitCriticalSection(void) { taskEXIT_CRITICAL(); } portevent.c - 事件标志 使用FreeRTOS事件组实现Modbus事件通知。\nportserial.c - 串口收发 实现串口初始化、收发控制，使用FIFO缓冲区接收数据。\nporttimer.c - 定时器 实现Modbus帧间隔检测，波特率大于19200时固定为1750us。\n主机移植 主机移植与从机类似，主要区别在于：\nportevent_m.c 中添加了响应事件标志 使用信号量实现主机操作同步 主机使用示例 void MasterTaskFun(void *argument) { for(;;) { eMBMasterPoll(); } } void MasterSendTaskFun(void *argument) { uint16_t data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; for(;;) { eMBMasterReqWriteMultipleHoldingRegister(1, 0, 10, data, 100); for (uint16_t i = 0; i \u0026lt; 10; i++) { data[i] += 1; } osDelay(1000); } } 从机使用示例 void Slave_TaskFun(void *argument) { for(;;) { eMBPoll(); } } 测试 使用Modbus Poll和Modbus Slave软件进行测试：\n配置连接参数（波特率、校验位等） 设置从机保持寄存器默认值 观察读写数据是否正确 完整代码 GitHub: https://github.com/freedom413/ModBusDemo\n包含Modbus Poll和Modbus Slave两个工程。\n","permalink":"/posts/stm32f407-modbus/","summary":"\u003ch1 id=\"准备工作\"\u003e准备工作\u003c/h1\u003e\n\u003ch2 id=\"工具介绍\"\u003e工具介绍\u003c/h2\u003e\n\u003ch3 id=\"软件\"\u003e软件\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eKeil5\u003c/strong\u003e: 代码编译调试\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCubeMX\u003c/strong\u003e: 代码生成工具\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVSCode\u003c/strong\u003e: 代码编辑器\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eModbus Poll \u0026amp;\u0026amp; Slave\u003c/strong\u003e: 调试工具\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"硬件\"\u003e硬件\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSTM32F407ZGT6\u003c/strong\u003e: 普中麒麟F407开发板\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e两个串口转USB\u003c/strong\u003e: 用于连接主机和从机\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"创建工程\"\u003e创建工程\u003c/h1\u003e\n\u003ch2 id=\"cubemx工程创建\"\u003eCubeMX工程创建\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e选择芯片型号\u003c/li\u003e\n\u003cli\u003e设置时钟（外部晶振）\u003c/li\u003e\n\u003cli\u003e设置调试接口和基准时钟\u003c/li\u003e\n\u003cli\u003e配置串口（USART1~3）\u003c/li\u003e\n\u003cli\u003e开启FreeRTOS\u003c/li\u003e\n\u003cli\u003e创建3个任务\u003c/li\u003e\n\u003cli\u003e生成项目\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"keil工程整合\"\u003eKeil工程整合\u003c/h2\u003e\n\u003cp\u003e将下载的Freemodbus源码复制到项目根目录，添加所需源文件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003emodbus/functions/\u003c/code\u003e 下所有文件\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emodbus/rtu/\u003c/code\u003e 下所有文件\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emb.c\u003c/code\u003e 和 \u003ccode\u003emb_m.c\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"移植详解\"\u003e移植详解\u003c/h1\u003e\n\u003ch2 id=\"portc---临界区实现\"\u003eport.c - 临界区实现\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e\u0026#34;FreeRTOS.h\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e\u0026#34;task.h\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e\u0026#34;semphr.h\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eEnterCriticalSection\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#268bd2\"\u003etaskENTER_CRITICAL\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eExitCriticalSection\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#268bd2\"\u003etaskEXIT_CRITICAL\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"porteventc---事件标志\"\u003eportevent.c - 事件标志\u003c/h2\u003e\n\u003cp\u003e使用FreeRTOS事件组实现Modbus事件通知。\u003c/p\u003e\n\u003ch2 id=\"portserialc---串口收发\"\u003eportserial.c - 串口收发\u003c/h2\u003e\n\u003cp\u003e实现串口初始化、收发控制，使用FIFO缓冲区接收数据。\u003c/p\u003e\n\u003ch2 id=\"porttimerc---定时器\"\u003eporttimer.c - 定时器\u003c/h2\u003e\n\u003cp\u003e实现Modbus帧间隔检测，波特率大于19200时固定为1750us。\u003c/p\u003e\n\u003ch1 id=\"主机移植\"\u003e主机移植\u003c/h1\u003e\n\u003cp\u003e主机移植与从机类似，主要区别在于：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eportevent_m.c\u003c/code\u003e 中添加了响应事件标志\u003c/li\u003e\n\u003cli\u003e使用信号量实现主机操作同步\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"主机使用示例\"\u003e主机使用示例\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eMasterTaskFun\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eargument) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003efor\u003c/span\u003e(;;) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eeMBMasterPoll\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eMasterSendTaskFun\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eargument) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#dc322f\"\u003euint16_t\u003c/span\u003e data[\u003cspan style=\"color:#2aa198\"\u003e10\u003c/span\u003e] \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e {\u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e2\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e3\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e4\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e5\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e6\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e7\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e8\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e9\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e10\u003c/span\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003efor\u003c/span\u003e(;;) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eeMBMasterReqWriteMultipleHoldingRegister\u003c/span\u003e(\u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e10\u003c/span\u003e, data, \u003cspan style=\"color:#2aa198\"\u003e100\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#719e07\"\u003efor\u003c/span\u003e (\u003cspan style=\"color:#dc322f\"\u003euint16_t\u003c/span\u003e i \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#719e07\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e10\u003c/span\u003e; i\u003cspan style=\"color:#719e07\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            data[i] \u003cspan style=\"color:#719e07\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eosDelay\u003c/span\u003e(\u003cspan style=\"color:#2aa198\"\u003e1000\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"从机使用示例\"\u003e从机使用示例\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eSlave_TaskFun\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eargument) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003efor\u003c/span\u003e(;;) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eeMBPoll\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"测试\"\u003e测试\u003c/h1\u003e\n\u003cp\u003e使用Modbus Poll和Modbus Slave软件进行测试：\u003c/p\u003e","title":"STM32F407移植FreemodbusV1.6"},{"content":"Freemodbus 简介 Freemodbus 是一款开源的 Modbus 协议栈，由armink大神移植。同时支持主机和从机的功能。\nFreeModbus 主机源码是开源的，这使得在嵌入式产品中实现Modbus主机功能变得简单。\n特性 新增加的主机源码与原有从机的风格及接口保持一致 支持主机与从机在同一协议栈运行 支持实时操作系统及裸机移植 为应用提供多种请求模式，可以选择阻塞还是非阻塞模式 支持所有常用的Modbus方法 文件结构 源文件 描述 FreeModbus/modbus/mb.c Modbus从机设置及轮询接口 FreeModbus/modbus/mb_m.c Modbus主机设置及轮询接口 FreeModbus/modbus/rtu/mbrtu.c 从机RTU模式设置 FreeModbus/modbus/rtu/mbrtu_m.c 主机RTU模式设置 FreeModbus/port/portserial.c 从机串口移植 FreeModbus/port/portserial_m.c 主机串口移植 FreeModbus/port/user_mb_app.c 从机数据缓冲区 FreeModbus/port/user_mb_app_m.c 主机数据缓冲区 数据缓冲区 数据缓冲区定义在 user_mb_app_m.c 文件中，共4种数据类型：\n线圈（Coils） 离散输入（Discrete Inputs） 保持寄存器（Holding Registers） 输入寄存器（Input Registers） API详解 写单个保持寄存器 eMBMasterReqWriteHoldingRegister( UCHAR ucSndAddr, USHORT usRegAddr, USHORT usRegData, LONG lTimeOut ); 读多个保持寄存器 eMBMasterReqReadHoldingRegister( UCHAR ucSndAddr, USHORT usRegAddr, USHORT usNRegs, LONG lTimeOut ); 读输入寄存器 eMBMasterReqReadInputRegister( UCHAR ucSndAddr, USHORT usRegAddr, USHORT usNRegs, LONG lTimeOut ); 返回值说明 返回值 描述 MB_MRE_NO_ERR 正常 MB_MRE_NO_REG 寄存器地址出错 MB_MRE_TIMEDOUT 响应超时 MB_MASTER_BUSY 主机忙 获取方式 GitHub: https://github.com/RT-Thread-packages/freemodbus\n","permalink":"/posts/freemodbus/","summary":"\u003ch1 id=\"freemodbus\"\u003eFreemodbus\u003c/h1\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003eFreemodbus 是一款开源的 Modbus 协议栈，由armink大神移植。同时支持主机和从机的功能。\u003c/p\u003e\n\u003cp\u003eFreeModbus 主机源码是开源的，这使得在嵌入式产品中实现Modbus主机功能变得简单。\u003c/p\u003e\n\u003ch2 id=\"特性\"\u003e特性\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e新增加的主机源码与原有从机的风格及接口保持一致\u003c/li\u003e\n\u003cli\u003e支持主机与从机在同一协议栈运行\u003c/li\u003e\n\u003cli\u003e支持实时操作系统及裸机移植\u003c/li\u003e\n\u003cli\u003e为应用提供多种请求模式，可以选择阻塞还是非阻塞模式\u003c/li\u003e\n\u003cli\u003e支持所有常用的Modbus方法\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"文件结构\"\u003e文件结构\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e源文件\u003c/th\u003e\n          \u003cth\u003e描述\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/modbus/mb.c\u003c/td\u003e\n          \u003ctd\u003eModbus从机设置及轮询接口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/modbus/mb_m.c\u003c/td\u003e\n          \u003ctd\u003eModbus主机设置及轮询接口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/modbus/rtu/mbrtu.c\u003c/td\u003e\n          \u003ctd\u003e从机RTU模式设置\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/modbus/rtu/mbrtu_m.c\u003c/td\u003e\n          \u003ctd\u003e主机RTU模式设置\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/port/portserial.c\u003c/td\u003e\n          \u003ctd\u003e从机串口移植\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/port/portserial_m.c\u003c/td\u003e\n          \u003ctd\u003e主机串口移植\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/port/user_mb_app.c\u003c/td\u003e\n          \u003ctd\u003e从机数据缓冲区\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFreeModbus/port/user_mb_app_m.c\u003c/td\u003e\n          \u003ctd\u003e主机数据缓冲区\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"数据缓冲区\"\u003e数据缓冲区\u003c/h2\u003e\n\u003cp\u003e数据缓冲区定义在 \u003ccode\u003euser_mb_app_m.c\u003c/code\u003e 文件中，共4种数据类型：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e线圈（Coils）\u003c/li\u003e\n\u003cli\u003e离散输入（Discrete Inputs）\u003c/li\u003e\n\u003cli\u003e保持寄存器（Holding Registers）\u003c/li\u003e\n\u003cli\u003e输入寄存器（Input Registers）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"api详解\"\u003eAPI详解\u003c/h2\u003e\n\u003ch3 id=\"写单个保持寄存器\"\u003e写单个保持寄存器\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#268bd2\"\u003eeMBMasterReqWriteHoldingRegister\u003c/span\u003e(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    UCHAR ucSndAddr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    USHORT usRegAddr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    USHORT usRegData,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    LONG lTimeOut\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"读多个保持寄存器\"\u003e读多个保持寄存器\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#268bd2\"\u003eeMBMasterReqReadHoldingRegister\u003c/span\u003e(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    UCHAR ucSndAddr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    USHORT usRegAddr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    USHORT usNRegs,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    LONG lTimeOut\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"读输入寄存器\"\u003e读输入寄存器\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#268bd2\"\u003eeMBMasterReqReadInputRegister\u003c/span\u003e(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    UCHAR ucSndAddr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    USHORT usRegAddr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    USHORT usNRegs,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    LONG lTimeOut\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"返回值说明\"\u003e返回值说明\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e返回值\u003c/th\u003e\n          \u003cth\u003e描述\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMB_MRE_NO_ERR\u003c/td\u003e\n          \u003ctd\u003e正常\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMB_MRE_NO_REG\u003c/td\u003e\n          \u003ctd\u003e寄存器地址出错\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMB_MRE_TIMEDOUT\u003c/td\u003e\n          \u003ctd\u003e响应超时\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMB_MASTER_BUSY\u003c/td\u003e\n          \u003ctd\u003e主机忙\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"获取方式\"\u003e获取方式\u003c/h2\u003e\n\u003cp\u003eGitHub: \u003ca href=\"https://github.com/RT-Thread-packages/freemodbus\"\u003ehttps://github.com/RT-Thread-packages/freemodbus\u003c/a\u003e\u003c/p\u003e","title":"Freemodbus主从库"},{"content":"四足机器人软硬件架构 🤦‍♀️飞书画的感觉不咋地\n四足机器人硬件（所有连接仅供参考，任何问题与本人无关） 电池 🐱‍👓请一定用万用表检测一下正负极是否和pcb接口对应以及电压是否正常，理论上来说2s~4s锂电池都可以接入 【淘宝】http://e.tb.cn/h.gKCOp83eZ0tq1GN?tk=drWN3hhtbwJ HU9196 「电动玩具枪锂电池7.4V充电器四驱越野遥控车充电电池挖掘机软弹枪」 点击链接直接打开 或者 淘宝搜索直接打开\n需要购买一套电池\n降压模块 👀保证至少可以过3A电流。 【淘宝】限时满20减2 http://e.tb.cn/h.gKaxrAQtpJIKUid?tk=qcLp3hhDj74 MF7997 「MP1584EN 3A 5A可调降压电源模块板稳压航模24V-12V 9V转5VDC-DC」 点击链接直接打开 或者 淘宝搜索直接打开\n需要购买1块降压模块\nNodeMcu 8266开发板 🤷‍♀️这个神板因该人手一个，不过现在乐鑫已经推出esp32c3了，新神板来了 【淘宝】限时官方立减0.73元 http://e.tb.cn/h.gKQFJ88xUs7VLfy?tk=jwn13hGPIay HU0854 「ESP8266串口WIFI模块 CP2102/CH340 NodeMCU Lua V3物联网开发板」 点击链接直接打开 或者 淘宝搜索直接打开\n需要购买1块开发板，esp8266 的硬件资源介绍及使用教程请自行学习\nSG90舵机 🐱‍🐉那家便宜买那家，基本都大差不差呀。 【淘宝】限时满88减2 http://e.tb.cn/h.gq7QGmTuJ59kCKV?tk=Wd5P3hGJuMC HU9196 「伺服舵机 SG90 MG90S 9g舵机 450直升飞机小车伺服器 180度 360度」 点击链接直接打开 或者 淘宝搜索直接打开\n需要购买8个舵机\n0.96 OLED 🐱这个也是人均有吧。 【淘宝】限时满88减2 http://e.tb.cn/h.gK4KNkc34hgFGe9?tk=9eZ13hGGVYG MF7997 「0.91/0.96/1.3寸白/黄/蓝色 双色 IIC通信小OLED显示屏模块液晶屏」 点击链接直接打开 或者 淘宝搜索直接打开\n需要购买1块屏\n结构件 💕3d建模当年也是我的最爱额。 Solidworks 建模装配体图 组装后和效果和模型差不多，不过组装后我只使用了一块pcb。\n3D打印使用的是导出stl格式文件，只需要打印四个关节和四个手部，底盘可以直接使用pcb，然后螺丝可以使用sg90舵机自带的。可以购买m2x10的螺柱螺母或者热熔胶来固定pcb和舵机，推荐使用热熔胶，因为热熔胶还可以用来加强oled以及舵机焊接口的连线\nPCB 🦄这个简单，花一个下午画的，能不能用不重要，干就完了 嘉立创eda简单设计了个底板主要是提供结构支撑，和连接降压，主控，电池，开关这几个模块。\n原理图就是吧购买的模块连接起来\n嘉立创工程文件和制造输出文件以及原理图，原理图全是模块设计，焊接简单。电池采用自带usb充电线充电。\n四足机器人软件 🧬东拼一点西凑一点，也是不负众望，成功堆出屎山代码，毕竟大家都说代码和人有一个能跑就行！ 上来先放源代码 在123网盘里可以直连下载YYDS,还有github的链接欢迎大家浏览我的github呀💖 所有资料连接 这里包括3d打印文件，pcb制造文件，嘉立创工程文件，以及所有源代码\n123yunpan: https://www.123pan.com/s/MB2CTd-814nA\ngithub: https://github.com/freedom413/miniroot 搭建好硬件，直接编译下载代码，然后需要编译下载一次文件系统的文件。就可以使用啦。\n开发平台 Vscode + PlatformIO 基于arduino vscode安装自行学习，在插件市场搜索PlatformIO 安装插件。可能需要开魔法才可以在建立工程时快速下载框架支持包。否则有可能需要等待数小时。\n打开提供的源代码。arduino 的使用方法请自行学习，资源很多。\n[env:esp12e] platform = espressif8266 board = nodemcuv2 framework = arduino board_build.filesystem = littlefs lib_deps = olikraus/U8g2@^2.35.19 ottowinter/ESPAsyncWebServer-esphome@^3.2.2 可以直接修改配置文件来加载库更改开发平台以及添加文件系统等，可以在platformio官网查看详细说明\nwifi网络服务器代码 使用的ap模式，通俗来说就是8266发射wifi信号手机来连接，手机浏览器访问8266的ip后，8266发送相应的资源到手机浏览器展示，当然8266也可以接收浏览器发送的信息。\nwifi连接的代码如下，wifi名字和密码可自行更改。\nconst char *wifiname = \u0026#34;Melody\u0026#34;; const char *wifipassword = \u0026#34;12345678\u0026#34;; #elif ApMod WiFi.mode(WIFI_AP); WiFi.softAP(wifiname, wifipassword); IP = WiFi.softAPIP().toString(); #endif 异步web服务器，可以在后台异步响应请求，从而不阻塞其它程序运行。\nstatic AsyncWebServer esp8266_server(80); // 建立网络服务器对象，该对象用于响应HTTP请求。监听端口（80） static void webserveInit(void) { if (LittleFS.begin()) { // 启动闪存文件系统 DebugPrt(\u0026#34;LittleFS Started\\n\u0026#34;); } else { DebugPrt(\u0026#34;LittleFS Failed to Start.\\n\u0026#34;); } esp8266_server.on(\u0026#34;/choosNum\u0026#34;, HTTP_GET, handlSetNum); //绑定函数 /choosNum 在网页get请求的资源地址 esp8266_server.onNotFound(handleUserRequest); //处理其它网络请求 来发送网页数据到客户端 // 启动网站服务 esp8266_server.begin(); DebugPrt(\u0026#34;HTTP server started\u0026#34;); } 其中LittleFS文件系统用于存储前端页面的htlm，css，javascript，还有图片等静态资源，存储在flash尾部空间中，这样可以大大减小ram的使用空间。 esp8266_server.on用于绑定接收数据处理函数，/choosNum 时请求地址，当客户端使用get方式请求这个地址时就会触发回调调用handlSetNum() 函数来处理。同理esp8266_server.onNotFound就时其余未知的地址都在这里回调执行通过handleUserRequest函数来处理。\n首先来看看handlSetNum函数的内容\nstatic void handlSetNum(AsyncWebServerRequest *request) { String res = request-\u0026gt;getParam(\u0026#34;num\u0026#34;)-\u0026gt;value(); num = res.toInt(); // 参考静态网页javascrpt xmlhttp.open(\u0026#34;GET\u0026#34;, \u0026#34;choosNum?num=\u0026#34;+num, true); get带参数请求 resflag = 1; // 接收到数据标志位 } 很简单，就是将接收到的数据中num的值获取到，然后将标志位置1表示接收到了数据。 然后handleUserRequest函数的内容就相对长一点了。 通过客户端请求的数据地址来找到数据，并发送给客户端，如果文件不存在就发送给客户端404 Not Found 文本。\nstatic void handleUserRequest(AsyncWebServerRequest *request) { // 获取用户请求资源地址(Request Resource） String reqResource = request-\u0026gt;url(); // 通过handleFileRead函数处处理用户请求资源 bool fileReadOK = handleFileRead(reqResource, request); // 如果在SPIFFS无法找到用户访问的资源，则回复404 (Not Found) if (!fileReadOK) { request-\u0026gt;send(404, \u0026#34;text/plain\u0026#34;, \u0026#34;404 Not Found\u0026#34;); } } 获取文件类型函数\nstatic String getContentType(String filename) { if (filename.endsWith(\u0026#34;.htm\u0026#34;)) return \u0026#34;text/html\u0026#34;; else if (filename.endsWith(\u0026#34;.html\u0026#34;)) return \u0026#34;text/html\u0026#34;; else if (filename.endsWith(\u0026#34;.css\u0026#34;)) return \u0026#34;text/css\u0026#34;; else if (filename.endsWith(\u0026#34;.js\u0026#34;)) return \u0026#34;application/javascript\u0026#34;; else if (filename.endsWith(\u0026#34;.png\u0026#34;)) return \u0026#34;image/png\u0026#34;; else if (filename.endsWith(\u0026#34;.gif\u0026#34;)) return \u0026#34;image/gif\u0026#34;; else if (filename.endsWith(\u0026#34;.jpg\u0026#34;)) return \u0026#34;image/jpeg\u0026#34;; else if (filename.endsWith(\u0026#34;.ico\u0026#34;)) return \u0026#34;image/x-icon\u0026#34;; else if (filename.endsWith(\u0026#34;.xml\u0026#34;)) return \u0026#34;text/xml\u0026#34;; else if (filename.endsWith(\u0026#34;.pdf\u0026#34;)) return \u0026#34;application/x-pdf\u0026#34;; else if (filename.endsWith(\u0026#34;.zip\u0026#34;)) return \u0026#34;application/x-zip\u0026#34;; else if (filename.endsWith(\u0026#34;.gz\u0026#34;)) return \u0026#34;application/x-gzip\u0026#34;; return \u0026#34;text/plain\u0026#34;; } static bool handleFileRead(String resource, AsyncWebServerRequest *request) { // 处理浏览器HTTP访问 if (resource.endsWith(\u0026#34;/\u0026#34;)) { // 如果访问地址以\u0026#34;/\u0026#34;为结尾 resource = \u0026#34;/index.html\u0026#34;; // 则将访问地址修改为/index.html便于SPIFFS访问 } String contentType = getContentType(resource); // 获取文件类型 if (LittleFS.exists(resource)) { // 如果访问的文件可以在SPIFFS中找到 LittleFS.open(resource, \u0026#34;r\u0026#34;); // 则尝试打开该文件 request-\u0026gt;send(LittleFS, resource, contentType); // 并且将该文件返回给浏览器 return true; // 返回true } return false; // 如果文件未找到，则返回false } 通过getContentType函数获取文件发送类型，就是通过请求文件后缀来得出返回数据的类型。LittleFS.exists(resource) 如果文件系统中存在这个文件，就将文件读出来发送给客户端。contentType就是解析出来的返回数据类型。\n静态网页代码 在data文件夹下存放的就是需要编译后存入flash通过LittleFS来管理的文件，htlm，css，javascript这些文件。\n文件需要编译后下载，步骤如下。\n一个简单的界面，一个图片和12个按钮组成，关于前端的知识可以在菜鸟教程去学习。\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;手机端控制页面\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;styles.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;header\u0026gt; \u0026lt;img src=\u0026#34;header-image.png\u0026#34; alt=\u0026#34;头部图片\u0026#34;\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;main class=\u0026#34;content-container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;grid-container\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button1\u0026#34; onclick=\u0026#34;sendData(1)\u0026#34;\u0026gt;站立\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button2\u0026#34; onclick=\u0026#34;sendData(2)\u0026#34;\u0026gt;前进\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button3\u0026#34; onclick=\u0026#34;sendData(3)\u0026#34;\u0026gt;来呀\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button4\u0026#34; onclick=\u0026#34;sendData(4)\u0026#34;\u0026gt;左转\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button5\u0026#34; onclick=\u0026#34;sendData(5)\u0026#34;\u0026gt;招手\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button6\u0026#34; onclick=\u0026#34;sendData(6)\u0026#34;\u0026gt;右转\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button7\u0026#34; onclick=\u0026#34;sendData(7)\u0026#34;\u0026gt;摇摆\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button8\u0026#34; onclick=\u0026#34;sendData(8)\u0026#34;\u0026gt;炒菜\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button9\u0026#34; onclick=\u0026#34;sendData(9)\u0026#34;\u0026gt;游泳\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button10\u0026#34; onclick=\u0026#34;sendData(10)\u0026#34;\u0026gt;俯撑\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button11\u0026#34; onclick=\u0026#34;sendData(11)\u0026#34;\u0026gt;动一\u0026lt;/button\u0026gt; \u0026lt;button class=\u0026#34;grid-item\u0026#34; id=\u0026#34;button12\u0026#34; onclick=\u0026#34;sendData(12)\u0026#34;\u0026gt;动二\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/main\u0026gt; \u0026lt;script src=\u0026#34;scripts.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 发送脚本 ：在按钮按下时调用，将按钮的传入值发送给服务器，值绑定到了num，使用Get方法发送。同样菜鸟教程也可以学习\nfunction sendData(num) { var xmlhttp; if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); } else { xmlhttp = new ActiveXObject(\u0026#34;Microsoft.XMLHTTP\u0026#34;); } xmlhttp.open(\u0026#34;GET\u0026#34;, \u0026#34;choosNum?num=\u0026#34;+num, true); xmlhttp.send(); } 最终在网页上展示的控制界面如下图\n可调速舵机代码 SG90舵机时通过周期20ms占空比0.5~2.5ms的pwm波来控制的，0度对应0.5ms ,180度对应2.5ms。 调速的主要思想就是，将一段距离需要在设定的时间内完成的位移，拆分成若干个周期来运行，计算出，总共需要多少个周期，然后将角度均分，每个周期写入一个增量的占空比，逐渐累加占空比即可达到调速的目的。\n简单的可以控制单个舵机调速的程序，同时控制多个舵机的设计思想也一样，就是需要在写法上做出多舵机操作接口。向上取整增量是为了防止增量为0时在循环中卡死。\n/** * @brief 设置单个关节 可以调速 * @param PIN 那个关节 * @param AG 要到达的角度 * @param ms 过程时间 */ void SetangelStep(RootPin PIN, int16_t AG, uint16_t ms) { int16_t count = ms / 20; // 20ms(pwm周期) 一个间隔 int16_t lastag = Getangelus(PIN); // 获取上一次的占空比时间us int16_t lastagbk = lastag; // 备份时间 AG = revvaule(AG, 2500, 500, 180, 0); // 量程转换 目标角度转换成目标占空比时间us int16_t dertag = 0; // 目标时间与当前时间的误差 // 算出最大误差 if (AG \u0026gt; lastag) { dertag = AG - lastag; } else { dertag = lastag - AG; } int16_t inc = (dertag - 1) / count + 1; // 计算出每次减小误差的 步进值 向上取整 while (1) { // 每次循环减小一次误差 if (AG \u0026gt; lastagbk) { lastag += inc; if (lastag \u0026gt;= AG) return; } else { lastag -= inc; if (lastag \u0026lt;= AG) return; } // 设定计算出的占空比时间 Setangelus(PIN, lastag); delay(20); // 保持一个周期 } } 完整的舵机调速程序如下\ntypedef struct { /*控制变量*/ uint16_t us; // 目标角度 -在运算时时转换后成us uint16_t dertms; // 设定速度(ms) - 在运算时为增量 uint8_t en; // 是否使能 RootPin BandPin; // 绑定的关节 /*运算过程变量*/ uint16_t usbak; // 最初的us备份 uint16_t nowus; // 当前的us } myservo_t; typedef void (*setfun)(RootPin, uint16_t); // 函数指针取别名 自己的舵机角度设定函数 /** * @brief 通过定义结构体来实现舵机的调速控制，支持自定义舵机数量，支持不同速度 * @param myservo_t *sr 舵机控制结构体，设定目标角度、到达目标的过程时间，是否使能，以及绑定引脚 * @param uint8_t nums 舵机数组的数量（长度） * @param setfun fun 舵机的执行函数 */ void SetangelStepSe(myservo_t *sr, uint8_t nums, setfun fun) { for (uint16_t i = 0; i \u0026lt; nums; i++) { // 计算目标us数 sr[i].us = revvaule(sr[i].us, 2500, 500, 180, 0); // 获取当前us数并备份 sr[i].nowus = sr[i].usbak = Getangelus(sr[i].BandPin); // 计算出增量 注意要向上取整，增量为0，在误差判断里会无限循环。 if (sr[i].us \u0026gt; sr[i].usbak) // 如果目标us大于当前us { sr[i].dertms = (sr[i].us - sr[i].usbak - 1) / (sr[i].dertms / 20 /* 计算可以循环多少个周期 */) + 1; } else { sr[i].dertms = (sr[i].usbak - sr[i].us - 1) / (sr[i].dertms / 20) + 1; } } while (1) { for (uint8_t i = 0; i \u0026lt; nums; i++) { // 每次循环减小一次误差 if (sr[i].us \u0026gt; sr[i].usbak) { sr[i].nowus += sr[i].dertms; if (sr[i].nowus \u0026gt;= sr[i].us) { sr[i].en = 0; } } else { sr[i].nowus -= sr[i].dertms; if (sr[i].nowus \u0026lt;= sr[i].us) { sr[i].en = 0; } } } uint8_t isret = 1; // 可以返回标志 for (uint8_t i = 0; i \u0026lt; nums; i++) { if (sr[i].en == 1) // 还没到达目标us { fun(sr[i].BandPin, sr[i].nowus); // 通过函数指针的函数 设定计算出的占空比时间 isret = 0; } } if (isret == 1) // 所有关节都到位退出 { return; } delay(20); // 保持一个周期 } } 其实如果自己需要自定义动作的话 只需要知道怎么使用这个可以调速的关节设定函数就可以了，我在注释中已经说明了然后，动则这些就需要你自己发挥想象空间来创造咯，相信你们的想象力一定更好了🙆‍♀️ /** * @brief设置 肩部关节 转动 可以调速。 * @param ag 目标角度 * @param ms 转动的时间 也就是平均速度啦 * @param en1 是否使能第一脚 * @param en2 是否使能第一脚 * @param en3 是否使能第一脚 * @param en4 是否使能第一脚 */ void SetAllSePinStep(uint16_t ag, uint16_t ms, uint8_t en1, uint8_t en2, uint8_t en3, uint8_t en4); void SetAllSiPinStep(uint16_t ag, uint16_t ms); void SetAllSiPinStep(uint16_t ag, uint16_t ms, uint8_t en1, uint8_t en2, uint8_t en3, uint8_t en4); 其他模块的代码都比较简单了，请自行学习查看。 值得一提的是，为了资源的合理利用，我在主循环中加入了最原始分时调度的代码，这也就是一些实时操作系统，最最基本的思想了吧。代码如下。\nvoid loop() { if (millis() - LedpreviousMillis \u0026gt;= ledtime) { LedpreviousMillis = millis(); // 运行内容 { ledRun();//翻转led } } else if (millis() - WebpreviousMillis \u0026gt;= webtime) { WebpreviousMillis = millis(); // 运行内容 { ALoop();//调度动作执行 } } } 两个任务在设定的间隔周期触发一次。不会一直轮询等待，可以减轻mcu的负担。其中millis()函数是表示当前系统运行的ms数。\n实物演示 让我等下再做了🐱‍🚀🐱‍👓🐱‍🏍🐱‍👤\n**ok呀ok来做了e-拍了个小视频，哈哈，直接看视频吧🐱‍👤！不对我还拍了图片，那先看高清美图吧！\n我给它来了个360无死角展示首先来展示它的正面\n然后是侧面\n然后看看屁股吧\n那小肚子也给你们看吧\n直接解刨给你看\n展示完毕上vlog\n{% raw %}\n{% endraw %} 总结 价格 物品 电池套件 降压模块 8266开发板 sg90舵机x8 0.96 oled 3D打印 总价 单价 15.2 4.55 9.65 4x8 = 32 6.1 18 85.5 学习到的东西 c语言更加熟练的使用\u0026ndash;其中包含了结构体，函数重载，typedef，函数指针，数组指针，static，等c语言知识。 arduino平台\u0026ndash;熟悉arduino平台架构，加强对库文件的使用和理解。 pcb制版\u0026ndash;(自行学习)通过这个小底板的绘制可以加强对pcb设计的流程掌握，还可以学习在一些小技巧等。 前端\u0026ndash;最基础的前端知识学习。 ","permalink":"/posts/diy-robot/","summary":"\u003ch1 id=\"四足机器人软硬件架构\"\u003e四足机器人软硬件架构\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e🤦‍♀️飞书画的感觉不咋地\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/diy-robot/img/%E6%9E%B6%E6%9E%84%E5%9B%BE.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"四足机器人硬件所有连接仅供参考任何问题与本人无关\"\u003e四足机器人硬件（所有连接仅供参考，任何问题与本人无关）\u003c/h1\u003e\n\u003ch2 id=\"电池\"\u003e电池\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e🐱‍👓请一定用万用表检测一下正负极是否和pcb接口对应以及电压是否正常，理论上来说2s~4s锂电池都可以接入\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e【淘宝】\u003ca href=\"http://e.tb.cn/h.gKCOp83eZ0tq1GN?tk=drWN3hhtbwJ\"\u003ehttp://e.tb.cn/h.gKCOp83eZ0tq1GN?tk=drWN3hhtbwJ\u003c/a\u003e HU9196 「电动玩具枪锂电池7.4V充电器四驱越野遥控车充电电池挖掘机软弹枪」\n点击链接直接打开 或者 淘宝搜索直接打开\u003c/p\u003e\n\u003cdiv align=center \u003e\n\u003cimg style=\"border: 2px solid black;border-radius: 5px;\" src=\"img/miniroot1.png\" width = 40%\u003e\n\u003cimg style=\"border: 2px solid black;border-radius: 5px;\" src=\"img/miniroot2.png\" width = 40%\u003e\n\u003c/div\u003e\n\u003cp\u003e\u003cstrong\u003e需要购买一套电池\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"降压模块\"\u003e降压模块\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e👀保证至少可以过3A电流。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e【淘宝】限时满20减2 \u003ca href=\"http://e.tb.cn/h.gKaxrAQtpJIKUid?tk=qcLp3hhDj74\"\u003ehttp://e.tb.cn/h.gKaxrAQtpJIKUid?tk=qcLp3hhDj74\u003c/a\u003e MF7997 「MP1584EN 3A 5A可调降压电源模块板稳压航模24V-12V 9V转5VDC-DC」\n点击链接直接打开 或者 淘宝搜索直接打开\u003c/p\u003e\n\u003cdiv align=center\u003e\n\u003cimg style=\"border: 2px solid black;border-radius: 5px;\" src=\"img/miniroot3.png\" width = 40%\u003e\n\u003cimg style=\"border: 2px solid black;border-radius: 5px;\" src=\"img/miniroot4.png\" width = 40%\u003e\n\u003c/div\u003e\n\u003cp\u003e\u003cstrong\u003e需要购买1块降压模块\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"nodemcu-8266开发板\"\u003eNodeMcu 8266开发板\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e🤷‍♀️这个神板因该人手一个，不过现在乐鑫已经推出\u003cstrong\u003eesp32c3\u003c/strong\u003e了，新神板来了\n【淘宝】限时官方立减0.73元 \u003ca href=\"http://e.tb.cn/h.gKQFJ88xUs7VLfy?tk=jwn13hGPIay\"\u003ehttp://e.tb.cn/h.gKQFJ88xUs7VLfy?tk=jwn13hGPIay\u003c/a\u003e HU0854 「ESP8266串口WIFI模块 CP2102/CH340 NodeMCU Lua V3物联网开发板」\n点击链接直接打开 或者 淘宝搜索直接打开\u003c/p\u003e\n\u003cdiv align=center\u003e\n\u003cimg style=\"border: 2px solid black;border-radius: 5px;\" src=\"img/miniroot5.png\" width = 40%\u003e\n\u003cimg style=\"border: 2px solid black;border-radius: 5px;\" src=\"img/miniroot6.png\" width = 40%\u003e\n\u003c/div\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e需要购买1块开发板，esp8266 的硬件资源介绍及使用教程请自行学习\u003c/strong\u003e\u003c/p\u003e","title":"DIY四足机器人"},{"content":"解读环形队列-C语言实现 环形队列（Circular Queue）是一种特殊的队列数据结构。与普通的线性队列不同，环形队列在物理存储上表现为一个首尾相连的环状结构，在逻辑上则仍然遵循先进先出（FIFO）的原则。\n特点 循环利用空间：当队列中的元素被删除后，原来的空间可以被重新使用 队头队尾指针：使用head和end两个指针标识队列的头部和尾部 队满与队空判断：需要特殊处理，通常预留一个位置不使用 动态调整：可以动态调整数组大小来适应数据量变化 应用场景 缓冲区管理：在网络编程或硬件驱动程序中管理固定大小的缓冲区 生产者消费者模型：在多线程环境中用作数据共享的数据结构 滑动窗口算法：实现高效的数据处理策略 串口接收：STM32中常用于串口接收，实现数据异步使用 图解 非循环队列 非循环队列由一个头指针head和一个尾指针end构成。\n入队操作：\nend = end + 1，然后将数据存入end指向的空间：\n出队操作：\n出队就是将head指向空间的数据抛出，然后head加一：\n判断队空：\nend = -1 表示队空 end + 1 = head 表示队空 判断队满：end + 1 = MAX_SIZE\n循环队列 循环队列的head指针并不指向数据头，而是指向上一次被删除的数据地址。\n下标范围控制：\nend = (end+1) % MAX_SIZE head = (head+1) % MAX_SIZE 入队操作：\n出队操作：\n判断队满：head = (end+1) % MAX_SIZE\n判断队空：head == end\n队空条件同样适用于循环后的情况：\nC语言实现 定义环形FIFO数据结构 #define Max_Size (10) typedef struct { uint32_t head; // 头指针 uint32_t end; // 尾指针 uint8_t data[Max_Size]; } Fifo_t; 初始化函数 void FifoInit(Fifo_t *f) { f-\u0026gt;end = 0; f-\u0026gt;head = 0; } 判断队列空函数 uint8_t isEmpty(Fifo_t *f) { return (f-\u0026gt;end == f-\u0026gt;head); } 判断队列满函数 uint8_t isFull(Fifo_t *f) { return ((f-\u0026gt;end + 1) % Max_Size == f-\u0026gt;head); } 入队函数 uint32_t FifoIn(Fifo_t *f, uint8_t *in, uint32_t len) { uint32_t i; for (i = 0; i \u0026lt; len; i++) { if (isFull(f)) { return 0; } f-\u0026gt;end = (f-\u0026gt;end + 1) % Max_Size; f-\u0026gt;data[f-\u0026gt;end] = in[i]; } return i; } 出队函数 uint8_t FifoOut(Fifo_t *f, uint8_t *out, uint32_t len) { uint32_t i; for (i = 0; i \u0026lt; len; i++) { if (isEmpty(f)) { return 0; } f-\u0026gt;head = (f-\u0026gt;head + 1) % Max_Size; out[i] = f-\u0026gt;data[f-\u0026gt;head]; } return i; } 完整源码 #include \u0026#34;stdio.h\u0026#34; #include \u0026#34;stdint.h\u0026#34; #define Max_Size (10) typedef struct { uint32_t head; uint32_t end; uint8_t data[Max_Size]; } Fifo_t; void FifoInit(Fifo_t *f) { f-\u0026gt;end = 0; f-\u0026gt;head = 0; } uint8_t isEmpty(Fifo_t *f) { return (f-\u0026gt;end == f-\u0026gt;head); } uint8_t isFull(Fifo_t *f) { return ((f-\u0026gt;end + 1) % Max_Size == f-\u0026gt;head); } uint32_t FifoIn(Fifo_t *f, uint8_t *in, uint32_t len) { uint32_t i; for (i = 0; i \u0026lt; len; i++) { if (isFull(f)) { return 0; } f-\u0026gt;end = (f-\u0026gt;end + 1) % Max_Size; f-\u0026gt;data[f-\u0026gt;end] = in[i]; } return i; } uint8_t FifoOut(Fifo_t *f, uint8_t *out, uint32_t len) { uint32_t i; for (i = 0; i \u0026lt; len; i++) { if (isEmpty(f)) { return 0; } f-\u0026gt;head = (f-\u0026gt;head + 1) % Max_Size; out[i] = f-\u0026gt;data[f-\u0026gt;head]; } return i; } 总结 环形队列有效地解决了普通队列空间浪费的问题，特别适合嵌入式系统中需要固定大小缓冲区的场景，如串口数据接收、网络通信等。\n","permalink":"/posts/ringfifo/","summary":"\u003ch1 id=\"解读环形队列-c语言实现\"\u003e解读环形队列-C语言实现\u003c/h1\u003e\n\u003cp\u003e环形队列（Circular Queue）是一种特殊的队列数据结构。与普通的线性队列不同，环形队列在物理存储上表现为一个首尾相连的环状结构，在逻辑上则仍然遵循先进先出（FIFO）的原则。\u003c/p\u003e\n\u003ch2 id=\"特点\"\u003e特点\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e循环利用空间\u003c/strong\u003e：当队列中的元素被删除后，原来的空间可以被重新使用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e队头队尾指针\u003c/strong\u003e：使用head和end两个指针标识队列的头部和尾部\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e队满与队空判断\u003c/strong\u003e：需要特殊处理，通常预留一个位置不使用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e动态调整\u003c/strong\u003e：可以动态调整数组大小来适应数据量变化\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"应用场景\"\u003e应用场景\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e缓冲区管理\u003c/strong\u003e：在网络编程或硬件驱动程序中管理固定大小的缓冲区\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e生产者消费者模型\u003c/strong\u003e：在多线程环境中用作数据共享的数据结构\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e滑动窗口算法\u003c/strong\u003e：实现高效的数据处理策略\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e串口接收\u003c/strong\u003e：STM32中常用于串口接收，实现数据异步使用\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"图解\"\u003e图解\u003c/h2\u003e\n\u003ch3 id=\"非循环队列\"\u003e非循环队列\u003c/h3\u003e\n\u003cp\u003e非循环队列由一个头指针head和一个尾指针end构成。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e入队操作\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"入队\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eend = end + 1\u003c/code\u003e，然后将数据存入end指向的空间：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"入队2\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e出队操作\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"出队\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e出队就是将head指向空间的数据抛出，然后head加一：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"出队2\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e判断队空\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eend = -1\u003c/code\u003e 表示队空\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eend + 1 = head\u003c/code\u003e 表示队空\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"队空\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_5.png\"\u003e \u003cimg alt=\"队空2\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_6.png\"\u003e \u003cimg alt=\"队空3\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e判断队满\u003c/strong\u003e：\u003ccode\u003eend + 1 = MAX_SIZE\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"队满\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_8.png\"\u003e \u003cimg alt=\"队满2\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_9.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"循环队列\"\u003e循环队列\u003c/h3\u003e\n\u003cp\u003e循环队列的head指针并不指向数据头，而是指向上一次被删除的数据地址。\u003c/p\u003e\n\u003cp\u003e下标范围控制：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eend = (end+1) % MAX_SIZE\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ehead = (head+1) % MAX_SIZE\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e入队操作\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"循环队列入队\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_10.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e出队操作\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"循环队列出队\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_11.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e判断队满\u003c/strong\u003e：\u003ccode\u003ehead = (end+1) % MAX_SIZE\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"循环队队满\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_13.png\"\u003e \u003cimg alt=\"循环队队满2\" loading=\"lazy\" src=\"/posts/ringfifo/img/ring_14.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e判断队空\u003c/strong\u003e：\u003ccode\u003ehead == end\u003c/code\u003e\u003c/p\u003e","title":"理解环形队列"},{"content":"SPI（Serial Peripheral Interface）是一种广泛应用的高速、全双工、同步串行通信协议。本质上是主从结构的总线协议，适用于MCU与各种外设间的短距离高速通信。\nSPI协议基础 硬件连接 SPI通信需要四根信号线：\n信号 全称 方向 说明 SCLK Serial Clock Master→Slave 时钟信号，由主机产生 MOSI Master Out Slave In Master→Slave 主机发送数据 MISO Master In Slave Out Slave→Master 主机接收数据 CS/SS Chip Select / Slave Select Master→Slave 片选信号，选择通信的从设备 MCU (Master) Slave Device ========== ============= +-------------+ +-------------+ | SCLK|-------------\u0026gt;|SCLK | | MOSI|-------------\u0026gt;|MOSI | | MISO|\u0026lt;-------------|MISO | | CS |-------------\u0026gt;|CS | +-------------+ +-------------+ 多从设备连接 MCU (Master) ========== +---------------+ | SCLK|-----------------------------------+ | MOSI|-----------------------------------+ | MISO|\u0026lt;----------------------------------+ | CS0 |-------------------+ | | CS1 |--------------------------+ | | CS2 |-----------------------------+ | +---------------+ | | | | | | | | v v v v v v +-----+-----+-----+ +-----+-----+-----+ | CS0 | CS1 | CS2 | | SS0 | SS1 | SS2 | +-----+-----+-----+ +-----+-----+-----+ 独立片选模式：每从设备独占一个CS引脚，最多可连接n个从设备 级联模式：多个从设备共享同一CS，数据线串联 SPI工作模式 SPI有四种工作模式，由时钟极性（CPOL）和时钟相位（CPHA）共同决定：\n时钟极性 (CPOL) CPOL 空闲时钟状态 时钟有效状态 0 低电平 高电平 1 高电平 低电平 时钟相位 (CPHA) CPHA 数据采样时机 数据输出时机 0 第一个时钟边沿 第二个时钟边沿 1 第二个时钟边沿 第一个时钟边沿 四种模式详解 /* * 四种SPI模式 * * CPOL=0, CPHA=0 (Mode 0) * - 空闲SCLK=0 * - 第一个边沿(上升沿)采样 * - 下降沿输出 * * CPOL=0, CPHA=1 (Mode 1) * - 空闲SCLK=0 * - 下降沿采样 * - 上升沿输出 * * CPOL=1, CPHA=0 (Mode 2) * - 空闲SCLK=1 * - 下降沿采样 * - 上升沿输出 * * CPOL=1, CPHA=1 (Mode 3) * - 空闲SCLK=1 * - 上升沿采样 * - 下降沿输出 */ // 常用模式0的时序 /* SCLK __|---|___|---|___|---|___|---|___ | | | | | | | | MOSI ---\u0026lt; D7 \u0026gt;--\u0026lt; D6 \u0026gt;--\u0026lt; D5 \u0026gt;--\u0026lt; D4 \u0026gt;--- | | | | | | | | MISO ---\u0026lt; D7 \u0026gt;--\u0026lt; D6 \u0026gt;--\u0026lt; D5 \u0026gt;--\u0026lt; D4 \u0026gt;--- ^ ^ ^ ^ 采样 采样 采样 采样 (上升沿) */ 模式选择指南 模式 CPOL CPHA 适用场景 Mode 0 0 0 最常用，大多数SPI器件默认模式 Mode 1 0 1 部分存储芯片 Mode 2 1 0 较少使用 Mode 3 1 1 高速通信，部分传感器 GPIO模拟SPI 当MCU硬件SPI不可用或需要更多控制时，可用GPIO模拟SPI协议：\n/* * GPIO模拟SPI - 基于STM32 HAL */ // 引脚定义 #define SPI_GPIO_PORT GPIOA #define SPI_SCK_PIN GPIO_PIN_5 #define SPI_MOSI_PIN GPIO_PIN_7 #define SPI_MISO_PIN GPIO_PIN_6 #define SPI_CS_PORT GPIOA #define SPI_CS_PIN GPIO_PIN_4 // GPIO初始化 void SPI_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能GPIO时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // SCK和MOSI配置为输出 GPIO_InitStruct.Pin = SPI_SCK_PIN | SPI_MOSI_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(SPI_GPIO_PORT, \u0026amp;GPIO_InitStruct); // MISO配置为输入 GPIO_InitStruct.Pin = SPI_MISO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(SPI_GPIO_PORT, \u0026amp;GPIO_InitStruct); // CS配置为输出 GPIO_InitStruct.Pin = SPI_CS_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(SPI_CS_PORT, \u0026amp;GPIO_InitStruct); // 初始状态：CS=1, SCK=0 HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); } // SPI模式0：CPOL=0, CPHA=0 void SPI_GPIO_Init_Mode0(void) { // Mode 0: 空闲SCK=0, 第一个边沿采样 HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); } // SPI模式3：CPOL=1, CPHA=1 void SPI_GPIO_Init_Mode3(void) { // Mode 3: 空闲SCK=1, 第二个边沿采样 HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET); } // 发送一个字节 void SPI_GPIO_SendByte(uint8_t data) { for (int i = 7; i \u0026gt;= 0; i--) { // 设置MOSI数据 if (data \u0026amp; (1 \u0026lt;\u0026lt; i)) { HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET); } // 产生时钟边沿 - Mode 0先采样再输出 // 实际上Mode 0第一个边沿(上升沿)采样，所以先拉高SCK再拉低 HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET); // 采样点 delay_us(1); HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); // 完成一位 delay_us(1); } } // 接收一个字节 uint8_t SPI_GPIO_RecvByte(void) { uint8_t data = 0; for (int i = 7; i \u0026gt;= 0; i--) { // Mode 0: 上升沿采样 HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET); delay_us(1); if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN) == GPIO_PIN_SET) { data |= (1 \u0026lt;\u0026lt; i); } HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); delay_us(1); } return data; } // 全双工收发 uint8_t SPI_GPIO_Transfer(uint8_t data) { uint8_t received = 0; for (int i = 7; i \u0026gt;= 0; i--) { // 设置MOSI if (data \u0026amp; (1 \u0026lt;\u0026lt; i)) { HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET); } // 时钟上升沿采样MISO HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET); delay_us(1); if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN)) { received |= (1 \u0026lt;\u0026lt; i); } // 时钟下降沿 HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); delay_us(1); } return received; } // 完整的SPI读写操作 uint16_t SPI_GPIO_WriteRead(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) { HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_RESET); for (uint16_t i = 0; i \u0026lt; len; i++) { rx_buf[i] = SPI_GPIO_Transfer(tx_buf[i]); } HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET); return len; } 硬件SPI配置 使用MCU内置的SPI控制器，效率更高：\n/* * STM32 HAL库硬件SPI配置 */ // SPI句柄 SPI_HandleTypeDef hspi1; // SPI1初始化 (APB2时钟 84MHz) void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // 主模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8位数据 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0, Mode 0 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 84/8=10.5MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先行 hspi1.Init.TIMode = SPI_TIMODE_DISABLE; // 非TI模式 hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 7; if (HAL_SPI_Init(\u0026amp;hspi1) != HAL_OK) { Error_Handler(); } } // SPI引脚配置 (复用功能) void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (hspi-\u0026gt;Instance == SPI1) { __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // PA5: SCK, PA6: MISO, PA7: MOSI GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, \u0026amp;GPIO_InitStruct); } } 典型外设驱动实现 Flash存储芯片驱动 (W25Qxx) /* * W25Q64 Flash芯片驱动 (SPI接口) * 容量: 8MB (64Mbit) */ #define W25Q_OK 0 #define W25Q_ERROR 1 // W25Qxx命令 #define W25Q_READ_STATUS_REG1 0x05 #define W25Q_WRITE_ENABLE 0x06 #define W25Q_WRITE_DISABLE 0x04 #define W25Q_READ_DATA 0x03 #define W25Q_FAST_READ 0x0B #define W25Q_PAGE_PROGRAM 0x02 #define W25Q_SECTOR_ERASE_4KB 0x20 #define W25Q_BLOCK_ERASE_64KB 0xD8 #define W25Q_CHIP_ERASE 0xC7 #define W25Q_JEDEC_ID 0x9F typedef struct { SPI_HandleTypeDef *hspi; GPIO_TypeDef *cs_port; uint16_t cs_pin; uint8_t status; } W25Qxx_HandleTypeDef; W25Qxx_HandleTypeDef g_w25q; // 初始化 int W25Q_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *cs_port, uint16_t cs_pin) { g_w25q.hspi = hspi; g_w25q.cs_port = cs_port; g_w25q.cs_pin = cs_pin; HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET); // 读取ID验证通信 uint8_t id[3]; W25Q_ReadJEDECID(id); if (id[0] != 0xEF || id[1] != 0x40 || id[2] != 0x17) { return W25Q_ERROR; } return W25Q_OK; } // 等待Flash空闲 void W25Q_WaitBusy(void) { uint8_t status; do { W25Q_ReadStatus(\u0026amp;status, 1); } while (status \u0026amp; 0x01); } // 读取状态寄存器 void W25Q_ReadStatus(uint8_t *status, uint8_t len) { HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET); uint8_t cmd = W25Q_READ_STATUS_REG1; HAL_SPI_Transmit(g_w25q.hspi, \u0026amp;cmd, 1, 100); HAL_SPI_Receive(g_w25q.hspi, status, len, 100); HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET); } // 发送命令 void W25Q_SendCommand(uint8_t cmd) { HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(g_w25q.hspi, \u0026amp;cmd, 1, 100); } // 读取JEDEC ID void W25Q_ReadJEDECID(uint8_t *id) { W25Q_SendCommand(W25Q_JEDEC_ID); HAL_SPI_Receive(g_w25q.hspi, id, 3, 100); HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET); } // 使能写操作 void W25Q_WriteEnable(void) { W25Q_SendCommand(W25Q_WRITE_ENABLE); HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET); } // 扇区擦除 (4KB) int W25Q_EraseSector(uint32_t addr) { W25Q_WaitBusy(); W25Q_WriteEnable(); uint8_t cmd[4] = {W25Q_SECTOR_ERASE_4KB, (addr \u0026gt;\u0026gt; 16) \u0026amp; 0xFF, (addr \u0026gt;\u0026gt; 8) \u0026amp; 0xFF, addr \u0026amp; 0xFF}; HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100); HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET); W25Q_WaitBusy(); return W25Q_OK; } // 页编程 (256字节) int W25Q_PageProgram(uint32_t addr, uint8_t *data, uint32_t len) { if (len \u0026gt; 256) return W25Q_ERROR; W25Q_WaitBusy(); W25Q_WriteEnable(); uint8_t cmd[4] = {W25Q_PAGE_PROGRAM, (addr \u0026gt;\u0026gt; 16) \u0026amp; 0xFF, (addr \u0026gt;\u0026gt; 8) \u0026amp; 0xFF, addr \u0026amp; 0xFF}; HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100); HAL_SPI_Transmit(g_w25q.hspi, data, len, 100); HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET); W25Q_WaitBusy(); return W25Q_OK; } // 读取数据 int W25Q_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { W25Q_WaitBusy(); uint8_t cmd[4] = {W25Q_READ_DATA, (addr \u0026gt;\u0026gt; 16) \u0026amp; 0xFF, (addr \u0026gt;\u0026gt; 8) \u0026amp; 0xFF, addr \u0026amp; 0xFF}; HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100); HAL_SPI_Receive(g_w25q.hspi, buf, len, 100); HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET); return W25Q_OK; } 常见问题与解决方案 1. 时钟速率过高 /* * 问题：时钟速率过高导致数据错乱 * * 原因： * - MCU时钟过快 * - 线缆过长 * - 从设备不支持高速 * * 解决：根据从设备最高支持频率降低波特率 */ // STM32分频设置 #define SPI_BAUDRATEPRESCALER_2 0x00 // 42MHz (最快) #define SPI_BAUDRATEPRESCALER_4 0x08 // 21MHz #define SPI_BAUDRATEPRESCALER_8 0x10 // 10.5MHz #define SPI_BAUDRATEPRESCALER_16 0x18 // 5.25MHz #define SPI_BAUDRATEPRESCALER_256 0x70 // ~328KHz (最慢) // 根据从设备选择合适分频 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 10.5MHz通常安全 2. 模式不匹配 /* * 问题：读取数据全为0xFF或错误值 * * 原因：主机与从机的CPOL/CPHA不匹配 * * 解决：仔细阅读从设备数据手册，确认正确模式 */ // 常见设备的SPI模式 /* * W25Qxx Flash: Mode 0 或 Mode 3 (支持两种) * SD卡: Mode 0 * NRF24L01: Mode 0 * OLED (SSD1306): Mode 0 * ADXL345: Mode 3 * MCP3008: Mode 0 */ // 调试建议：先用示波器观察SCLK和MOSI/MISO波形 3. CS片选时序问题 /* * 问题：片选时序不对导致通信失败 * * 常见问题： * - 片选提前拉高 * - 片选持续时间不够 * - 多字节传输时片选中途释放 */ // 正确做法：整个传输过程保持CS低 void SPI_TransferFull(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) { HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); // CS低 HAL_SPI_TransmitReceive(\u0026amp;hspi1, tx_buf, rx_buf, len, 1000); // 等待传输完成 while (HAL_SPI_GetState(\u0026amp;hspi1) != HAL_SPI_STATE_READY); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); // CS高 } // 反面例子：多字节分开传输 void BAD_SpiTransfer(uint8_t *data, uint16_t len) { for (uint16_t i = 0; i \u0026lt; len; i++) { HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(\u0026amp;hspi1, \u0026amp;data[i], 1, 100); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); // 错误！每字节CS都释放 } } 4. 数据对齐问题 /* * 问题：16位/32位数据通信时高低字节错位 * * 原因：大小端(MSB/LSB)设置不一致 * * 解决：统一设置为高位先行(MSB First) */ hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先行 // 验证方法：发送0x80，观察第一位是否正确 uint8_t test = 0x80; // 二进制: 10000000 // 如果第一位是1，则MSB优先正确 性能优化 DMA加速 /* * 使用DMA进行SPI传输，减少CPU干预 */ SPI_HandleTypeDef hspi1; DMA_HandleTypeDef hdma_spi1_tx; DMA_HandleTypeDef hdma_spi1_rx; // DMA初始化 void SPI_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_spi1_tx.Instance = DMA2_Stream3; hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3; hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_spi1_tx.Init.Mode = DMA_NORMAL; hdma_spi1_tx.Init.Priority = DMA_PRIORITY_MEDIUM; HAL_DMA_Init(\u0026amp;hdma_spi1_tx); __HAL_LINKDMA(\u0026amp;hspi1, hdmatx, hdma_spi1_tx); // 同样配置RX DMA... } // DMA传输 HAL_StatusTypeDef SPI_TransmitDMA(uint8_t *data, uint16_t len) { return HAL_SPI_Transmit_DMA(\u0026amp;hspi1, data, len); } // DMA完成回调 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { // 传输完成，可以通知其他任务 } 总结 使用SPI协议的关键点：\n确认工作模式：仔细阅读从设备数据手册，选择正确的CPOL和CPHA 控制时钟速率：确保不超过从设备支持的最大频率 注意片选时序：整个传输过程保持CS有效 大小端一致：主机与从机的位顺序要匹配 错误处理：实现超时检测和错误恢复机制 希望这篇深度解析能帮助你更好地掌握SPI通信协议！\n","permalink":"/posts/spi-protocol-guide/","summary":"\u003cp\u003eSPI（Serial Peripheral Interface）是一种广泛应用的高速、全双工、同步串行通信协议。本质上是主从结构的总线协议，适用于MCU与各种外设间的短距离高速通信。\u003c/p\u003e\n\u003ch2 id=\"spi协议基础\"\u003eSPI协议基础\u003c/h2\u003e\n\u003ch3 id=\"硬件连接\"\u003e硬件连接\u003c/h3\u003e\n\u003cp\u003eSPI通信需要四根信号线：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e信号\u003c/th\u003e\n          \u003cth\u003e全称\u003c/th\u003e\n          \u003cth\u003e方向\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eSCLK\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eSerial Clock\u003c/td\u003e\n          \u003ctd\u003eMaster→Slave\u003c/td\u003e\n          \u003ctd\u003e时钟信号，由主机产生\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eMOSI\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eMaster Out Slave In\u003c/td\u003e\n          \u003ctd\u003eMaster→Slave\u003c/td\u003e\n          \u003ctd\u003e主机发送数据\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eMISO\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eMaster In Slave Out\u003c/td\u003e\n          \u003ctd\u003eSlave→Master\u003c/td\u003e\n          \u003ctd\u003e主机接收数据\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCS/SS\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eChip Select / Slave Select\u003c/td\u003e\n          \u003ctd\u003eMaster→Slave\u003c/td\u003e\n          \u003ctd\u003e片选信号，选择通信的从设备\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        MCU (Master)                Slave Device\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ==========                  =============\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    +-------------+               +-------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |         SCLK|-------------\u0026gt;|SCLK         |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |         MOSI|-------------\u0026gt;|MOSI         |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |         MISO|\u0026lt;-------------|MISO         |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |          CS |-------------\u0026gt;|CS           |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    +-------------+               +-------------+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"多从设备连接\"\u003e多从设备连接\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e         MCU (Master)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e         ==========\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    +---------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |           SCLK|-----------------------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |           MOSI|-----------------------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |           MISO|\u0026lt;----------------------------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |            CS0 |-------------------+              |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |            CS1 |--------------------------+       |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    |            CS2 |-----------------------------+   |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    +---------------+                                |  |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        |   |   |                                |   |  |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        v   v   v                                v   v  v\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    +-----+-----+-----+                     +-----+-----+-----+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    | CS0 | CS1 | CS2 |                     | SS0 | SS1 | SS2 |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    +-----+-----+-----+                     +-----+-----+-----+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e独立片选模式\u003c/strong\u003e：每从设备独占一个CS引脚，最多可连接n个从设备\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e级联模式\u003c/strong\u003e：多个从设备共享同一CS，数据线串联\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"spi工作模式\"\u003eSPI工作模式\u003c/h2\u003e\n\u003cp\u003eSPI有四种工作模式，由时钟极性（CPOL）和时钟相位（CPHA）共同决定：\u003c/p\u003e","title":"SPI通信协议深度解析"},{"content":"在嵌入式系统中，内存资源通常非常有限，合理的内存管理策略直接影响系统的稳定性、可靠性和性能。本文将深入探讨嵌入式系统中的各种内存管理技术，帮助你设计出更高效的内存使用方案。\n嵌入式内存架构 在深入了解内存管理之前，先明确嵌入式系统的内存布局：\n+-------------------+ 0x00000000 | Flash | (代码和常量) | (只读存储器) | +-------------------+ 0xXXXXXXXX | RAM | (读写数据) | - .data | (已初始化全局变量) | - .bss | (未初始化全局变量) | - Heap | (动态分配) | - Stack | (函数调用、局部变量) +-------------------+ 0xXXXXXXXX 静态内存分配 静态内存分配在编译时确定，不会分配失败，无内存碎片，是嵌入式开发的首选方案。\n全局变量与静态变量 /* * 全局变量 - 整个程序生命周期内存在 * 位于 .data 或 .bss 段 */ // 已初始化全局变量 (.data段) uint32_t system_tick = 0; char device_name[] = \u0026#34;STM32F407\u0026#34;; // 未初始化全局变量 (.bss段，系统自动清零) static uint32_t error_count; uint8_t buffer[256]; // 静态局部变量 - 函数退出后保留值 uint32_t get_sequence_id(void) { static uint32_t id = 0; // 只初始化一次 return ++id; } 常量与只读数据 /* * const数据通常放在Flash中，节省RAM空间 */ // 字符串常量 - 存储在Flash const char *HELLO_MSG = \u0026#34;Hello, Embedded World!\u0026#34;; const char *VERSION = \u0026#34;V1.0.0\u0026#34;; // 查表法 - 将计算结果预先存储 const uint16_t sin_table[361] = { 0, 28, 57, 85, 114, ... // sin(angle) * 1000 }; // 配置表 - 存储设备参数 const DeviceConfig default_config = { .baudrate = 115200, .parity = 0, .stop_bits = 1, .timeout_ms = 1000 }; 静态内存池 预分配固定大小的内存块，分配和释放速度快，无碎片：\n/* * 静态内存池实现 * 适用于固定大小对象的频繁分配/释放 */ #define POOL_BLOCK_SIZE 64 #define POOL_BLOCK_COUNT 16 typedef struct BlockHeader { struct BlockHeader *next; uint8_t used; } BlockHeader; typedef struct { uint8_t pool[POOL_BLOCK_COUNT][POOL_BLOCK_SIZE]; BlockHeader *free_list; uint32_t total_size; uint32_t used_count; } StaticPool; // 初始化内存池 void Pool_Init(StaticPool *pool) { pool-\u0026gt;free_list = NULL; pool-\u0026gt;used_count = 0; pool-\u0026gt;total_size = POOL_BLOCK_COUNT * POOL_BLOCK_SIZE; // 构建空闲链表 for (int i = 0; i \u0026lt; POOL_BLOCK_COUNT; i++) { BlockHeader *block = (BlockHeader *)pool-\u0026gt;pool[i]; block-\u0026gt;next = pool-\u0026gt;free_list; block-\u0026gt;used = 0; pool-\u0026gt;free_list = block; } } // 分配内存块 void *Pool_Allocate(StaticPool *pool) { if (pool-\u0026gt;free_list == NULL) { return NULL; // 内存池已满 } BlockHeader *block = pool-\u0026gt;free_list; pool-\u0026gt;free_list = block-\u0026gt;next; block-\u0026gt;used = 1; pool-\u0026gt;used_count++; return (uint8_t *)block + sizeof(BlockHeader); } // 释放内存块 void Pool_Free(StaticPool *pool, void *ptr) { if (ptr == NULL) return; BlockHeader *block = (BlockHeader *)((uint8_t *)ptr - sizeof(BlockHeader)); block-\u0026gt;used = 0; block-\u0026gt;next = pool-\u0026gt;free_list; pool-\u0026gt;free_list = block; pool-\u0026gt;used_count--; } // 使用示例 StaticPool msg_pool; void Module_Init(void) { Pool_Init(\u0026amp;msg_pool); // 分配消息缓冲 Message *msg = (Message *)Pool_Allocate(\u0026amp;msg_pool); if (msg) { msg-\u0026gt;type = TYPE_DATA; msg-\u0026gt;len = 0; // ... } // 使用完毕释放 Pool_Free(\u0026amp;msg_pool, msg); } 动态内存分配 动态内存分配（堆管理）提供了更大的灵活性，但需要谨慎使用。\n堆管理的基本概念 /* * 堆（Heap）是可用的RAM区域 * malloc()从堆中分配内存 * free()将内存归还堆 */ #include \u0026lt;stdlib.h\u0026gt; void *pvPortMalloc(size_t xSize); // FreeRTOS版本 void vPortFree(void *pv); // FreeRTOS版本 // 基本使用 void *buffer = malloc(256); if (buffer != NULL) { // 使用buffer free(buffer); } // FreeRTOS中的内存分配 void *pvPortMalloc(size_t xSize) { // 线程安全实现 } 动态内存池（Heap脾） /* * FreeRTOS的heap_4.c使用最佳匹配算法 * 更好的内存利用率，更少的碎片 */ // 配置堆大小 (FreeRTOSConfig.h) #define configTOTAL_HEAP_SIZE (1024 * 64) // 64KB堆 // 创建队列时自动分配 QueueHandle_t xQueue = xQueueCreate(10, sizeof(Message)); // 内部调用pvPortMalloc() // 创建任务时指定栈大小 (不是堆) xTaskCreate( TaskFunction_t pvTaskCode, const char *pcName, uint16_t usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pxCreatedTask ); // 注意：任务栈在创建时分配，不占用堆 内存分配算法 /* * 最佳匹配算法 (Best Fit) * heap_4.c 使用此算法 * * 分配策略： * 1. 遍历空闲链表，找到最接近需求大小的块 * 2. 分配后剩余空间形成新的空闲块 */ // 空闲块结构 typedef struct ABlockLink { struct ABlockLink *pxNextFreeBlock; size_t xBlockSize; } BlockLink_t; // 空闲块示例 /* * +--------+--------+----------------+ * | Header | Header | | * | Size=80| Size=32| ... | * +--------+--------+----------------+ * ^ ^ ^ * | | | * 空闲块1 空闲块2 已分配块 */ // 分配过程：需要40字节 // 遍历找到Size=48的块（最小满足需求的） // 分割后：40字节分配，8字节剩余形成新区块 栈管理 栈是函数调用、局部变量和上下文保存的关键内存区域。\n栈的作用 /* * 栈的主要用途： * 1. 函数调用的返回地址 * 2. 函数参数传递 * 3. 局部变量存储 * 4. 寄存器上下文保存 */ int factorial(int n) { int result; if (n \u0026lt;= 1) { return 1; } // 递归调用 - 每层递归占用栈空间 result = n * factorial(n - 1); return result; } void process_data(void) { uint8_t buffer[64]; // 局部数组，栈上分配 uint32_t counter; // 局部变量 struct Complex local; // 结构体局部变量 // 栈上计算 for (int i = 0; i \u0026lt; 64; i++) { buffer[i] = process_byte(i); } } 栈溢出检测 栈溢出是嵌入式系统常见的问题，可能导致系统崩溃或数据损坏。\n/* * 方法1：填充模式检测 * 在任务创建时填充已知模式，运行时检查 */ #define STACK_FILL_BYTE 0xA5 StackType_t *pxTaskStack = (StackType_t *)pvPortMallocStack( usStackDepth * sizeof(StackType_t) ); if (pxTaskStack != NULL) { // 填充栈空间 for (uint32_t i = 0; i \u0026lt; usStackDepth; i++) { pxTaskStack[i] = STACK_FILL_BYTE; } pxNewTCB-\u0026gt;pxStack = pxTaskStack; } // 检查栈使用 uint32_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask) { StackType_t *pxStack = xTask-\u0026gt;pxStack; uint32_t count = 0; // 从栈底向上查找未使用的填充区域 while (*pxStack++ == STACK_FILL_BYTE) { count++; } return count * sizeof(StackType_t); } /* * 方法2：MPU保护（高端MCU） * 使用ARM Cortex-M的MPU设置栈区域保护 */ void vPortSetupMPU(void) { // 配置MPU区域 MPU-\u0026gt;RNR = 7; // 使用区域7 MPU-\u0026gt;RBAR = (uint32_t)pxStack \u0026amp; ~0x1F; MPU-\u0026gt;RASR = MPU_RASR_ENABLE_Msk | MPU_REGION_STACK_SIZE | MPU_RASR_AP_PRIV_RW; } 递归与栈深度 /* * 递归函数栈空间计算 * * 假设： * - 每个栈帧约32字节 * - 最大递归深度=100 * - 需要栈空间 = 100 * 32 = 3200字节 */ uint32_t calculate_stack_usage(uint32_t depth) { // 估算当前调用深度的栈使用 StackType_t highWater = uxTaskGetStackHighWaterMark(NULL); return (usStackDepth - highWater) * sizeof(StackType_t); } // 安全的递归实现 - 尾递归优化 uint32_t factorial_tail(uint32_t n, uint32_t acc) { if (n \u0026lt;= 1) { return acc; } return factorial_tail(n - 1, n * acc); // 尾递归 } // 或者改用迭代避免栈溢出 uint32_t factorial_iterative(uint32_t n) { uint32_t result = 1; for (uint32_t i = 2; i \u0026lt;= n; i++) { result *= i; } return result; } 内存优化技巧 1. 利用const节省RAM /* * const数据放在Flash中，不占用RAM */ // 不好：占用RAM const char *message = \u0026#34;Hello\u0026#34;; // 指针在RAM，指针指向的内容也在RAM // 好：内容放在Flash const char message[] = \u0026#34;Hello\u0026#34;; // 整个数组在Flash // 更好：明确指定存储位置 const char message[] PROGMEM = \u0026#34;Hello\u0026#34;; // AVR单片机，Flash存储 2. 使用位域节省结构体空间 /* * 位域允许将多个标志打包到单个字节 */ // 普通结构体 - 占用4字节 typedef struct { uint8_t flags1; // 1字节 uint8_t flags2; // 1字节 uint8_t flags3; // 1字节 uint8_t flags4; // 1字节 } Flags_t; // 位域结构体 - 占用2字节 typedef struct { uint8_t ready : 1; // bit0 uint8_t error : 1; // bit1 uint8_t busy : 1; // bit2 uint8_t overflow : 1; // bit3 uint8_t channel : 4; // bit4-7 } Flags_t; // 使用 Flags_t flag; flag.ready = 1; flag.channel = 0x0A; 3. 合理使用联合体 /* * 联合体让多个成员共享同一段内存 * 节省空间，用于同一数据的不同解释 */ typedef union { uint32_t word; uint16_t half[2]; uint8_t byte[4]; } DataUnion_t; DataUnion_t data; data.word = 0x12345678; printf(\u0026#34;Word: 0x%08X\\n\u0026#34;, data.word); // 0x12345678 printf(\u0026#34;High: 0x%04X\\n\u0026#34;, data.half[1]); // 0x1234 printf(\u0026#34;Low: 0x%04X\\n\u0026#34;, data.half[0]); // 0x5678 printf(\u0026#34;Byte3: 0x%02X\\n\u0026#34;, data.byte[3]); // 0x12 // 应用：协议解析 typedef union { uint32_t raw; struct { uint32_t type : 4; uint32_t cmd : 8; uint32_t addr : 16; uint32_t value : 4; } bits; } Packet_t; 4. 变量类型选择 /* * 选择合适的数据类型，避免浪费 */ // 不浪费：使用最小类型 uint8_t flags = 0; // 0-255 uint16_t counter = 0; // 0-65535 uint32_t timestamp = 0; // 大数值 // 数组优化 int8_t temperature[8]; // -128 ~ 127，温度范围足够 uint16_t adc_value[10]; // 0-4095，ADC值 // 浮点数慎用：占用4字节(32位)或8字节(64位) // 如果能用整数代替，优先用整数 // 例如：存储2位小数 * 100 用uint16_t 内存管理常见错误 1. 内存泄漏 /* * 分配后未释放，导致内存逐渐耗尽 */ // 错误示例 void task_leak(void *param) { uint8_t *buffer = malloc(256); // 每次调用都分配 // ... 处理数据 // 忘记free！ vTaskDelay(pdMS_TO_TICKS(100)); } // 如果任务每秒调用一次，64KB堆将在4分钟内耗尽 // 正确做法 void task_correct(void *param) { uint8_t *buffer = pvPortMalloc(256); if (buffer != NULL) { // ... 处理数据 vPortFree(buffer); // 释放内存 } } 2. 越界访问 /* * 访问数组边界之外的数据 */ // 错误示例 uint8_t buffer[10]; for (int i = 0; i \u0026lt;= 10; i++) { // i=10时越界！ buffer[i] = i; } // 正确 for (int i = 0; i \u0026lt; 10; i++) { buffer[i] = i; } // 使用安全函数 memset(buffer, 0, sizeof(buffer)); memcpy(dest, src, MIN(len, MAX_SIZE)); 3. 使用已释放内存 /* * 野指针问题 */ // 错误示例 void task_double_free(void) { char *ptr = malloc(32); vPortFree(ptr); vPortFree(ptr); // 双重释放！ // 指针未置NULL strcpy(ptr, \u0026#34;data\u0026#34;); // 使用已释放内存！ } // 正确做法 char *ptr = pvPortMalloc(32); if (ptr != NULL) { // 使用ptr vPortFree(ptr); ptr = NULL; // 释放后立即置NULL } 4. 栈溢出 /* * 局部变量占用过多栈空间 */ // 错误示例 - 大数组在栈上 void task_stack_overflow(void *param) { uint8_t large_buffer[4096]; // 4KB栈，可能溢出！ uint32_t matrix[100][100]; // 40KB栈，必定溢出！ // ... } // 正确做法 - 使用静态或堆分配 static uint8_t static_buffer[4096]; // 静态分配，不占用栈 uint8_t *heap_buffer = pvPortMalloc(4096); // 堆分配 if (heap_buffer == NULL) { // 处理分配失败 } 内存保护单元（MPU） 高端ARM Cortex-M微控制器提供MPU用于内存保护：\n/* * MPU配置示例 - STM32 */ // 定义可访问的内存区域 typedef struct { uint32_t base_addr; uint8_t size; // 2^size 字节 uint8_t access; // 访问权限 } MPU_Region_t; MPU_Region_t regions[] = { {0x08000000, 23, MPU_REGION_PRIV_RO}, // Flash: 8MB，只读 {0x20000000, 17, MPU_REGION_PRIV_RW}, // SRAM: 128KB，读写 {0x40000000, 16, MPU_REGION_DEV}, // 外设区 }; void MPU_Config(void) { MPU-\u0026gt;CTRL = 0; // 禁用MPU进行配置 for (int i = 0; i \u0026lt; sizeof(regions)/sizeof(regions[0]); i++) { MPU-\u0026gt;RNR = i; MPU-\u0026gt;RBAR = regions[i].base_addr; MPU-\u0026gt;RASR = MPU_RASR_ENABLE_Msk | (regions[i].size \u0026lt;\u0026lt; MPU_RASR_SIZE_Pos) | (regions[i].access \u0026lt;\u0026lt; MPU_RASR_AP_Pos); } MPU-\u0026gt;CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_HFNMIENA_Msk; } 总结 嵌入式内存管理的核心原则：\n能静态则静态 - 编译时确定的内存分配最可靠 内存池优先 - 频繁分配/释放的场景使用内存池 监控栈使用 - 使用高水位标记检测栈溢出风险 合理规划 - 根据系统需求选择合适的分配策略 避免常见错误 - 防止泄漏、越界、野指针等问题 深入理解内存管理，是编写可靠嵌入式代码的基础。希望本文对你有所帮助！\n","permalink":"/posts/embedded-memory-management/","summary":"\u003cp\u003e在嵌入式系统中，内存资源通常非常有限，合理的内存管理策略直接影响系统的稳定性、可靠性和性能。本文将深入探讨嵌入式系统中的各种内存管理技术，帮助你设计出更高效的内存使用方案。\u003c/p\u003e\n\u003ch2 id=\"嵌入式内存架构\"\u003e嵌入式内存架构\u003c/h2\u003e\n\u003cp\u003e在深入了解内存管理之前，先明确嵌入式系统的内存布局：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e+-------------------+  0x00000000\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|     Flash         |  (代码和常量)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|   (只读存储器)      |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e+-------------------+  0xXXXXXXXX\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|     RAM           |  (读写数据)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|   - .data         |  (已初始化全局变量)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|   - .bss          |  (未初始化全局变量)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|   - Heap          |  (动态分配)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e|   - Stack         |  (函数调用、局部变量)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e+-------------------+  0xXXXXXXXX\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"静态内存分配\"\u003e静态内存分配\u003c/h2\u003e\n\u003cp\u003e静态内存分配在编译时确定，不会分配失败，无内存碎片，是嵌入式开发的首选方案。\u003c/p\u003e\n\u003ch3 id=\"全局变量与静态变量\"\u003e全局变量与静态变量\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e/*\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e * 全局变量 - 整个程序生命周期内存在\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e * 位于 .data 或 .bss 段\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 已初始化全局变量 (.data段)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e system_tick \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003echar\u003c/span\u003e device_name[] \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e\u0026#34;STM32F407\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 未初始化全局变量 (.bss段，系统自动清零)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e error_count;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003euint8_t\u003c/span\u003e buffer[\u003cspan style=\"color:#2aa198\"\u003e256\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 静态局部变量 - 函数退出后保留值\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eget_sequence_id\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e id \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e;  \u003cspan style=\"color:#586e75\"\u003e// 只初始化一次\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    \u003cspan style=\"color:#719e07\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e++\u003c/span\u003eid;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"常量与只读数据\"\u003e常量与只读数据\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e/*\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e * const数据通常放在Flash中，节省RAM空间\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 字符串常量 - 存储在Flash\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#dc322f\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eHELLO_MSG \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e\u0026#34;Hello, Embedded World!\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#dc322f\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eVERSION \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e\u0026#34;V1.0.0\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 查表法 - 将计算结果预先存储\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#dc322f\"\u003euint16_t\u003c/span\u003e sin_table[\u003cspan style=\"color:#2aa198\"\u003e361\u003c/span\u003e] \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e28\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e57\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e85\u003c/span\u003e, \u003cspan style=\"color:#2aa198\"\u003e114\u003c/span\u003e, ...  \u003cspan style=\"color:#586e75\"\u003e// sin(angle) * 1000\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 配置表 - 存储设备参数\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003econst\u003c/span\u003e DeviceConfig default_config \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .baudrate \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e115200\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .parity \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .stop_bits \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    .timeout_ms \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e1000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"静态内存池\"\u003e静态内存池\u003c/h3\u003e\n\u003cp\u003e预分配固定大小的内存块，分配和释放速度快，无碎片：\u003c/p\u003e","title":"嵌入式系统内存管理详解"},{"content":"实时操作系统（RTOS）的核心价值在于提供确定性的响应时间和任务调度能力。作为嵌入式开发者，理解RTOS的任务调度原理对于合理设计任务优先级、避免优先级反转、优化系统性能至关重要。\n任务与进程 在RTOS中，\u0026ldquo;任务\u0026rdquo;（Task）通常对应一个执行流。与传统操作系统的进程不同：\n特性 进程 RTOS任务 地址空间 隔离 共享 栈空间 独立 独立 资源隔离 强 弱 创建/切换开销 大 小 RTOS任务更像是轻量级的线程，共享相同的地址空间，但拥有独立的栈和上下文。\n任务控制块（TCB） 任务控制块（TCB - Task Control Block）是RTOS管理任务的核心数据结构：\ntypedef struct tcb { volatile void *sp; // 栈指针 uint32_t priority; // 任务优先级 uint16_t stack_size; // 栈大小 StackType_t *stack_base; // 栈底 // 状态信息 TaskState state; uint32_t time_slice; // 时间片计数 uint32_t tick_count; // 滴答计数 // 链表节点（用于就绪队列/阻塞队列） ListItem_t state_list_item; ListItem_t event_list_item; // 任务函数 TaskFunction_t func; void *param; // 任务名（用于调试） char name[configMAX_TASK_NAME_LEN]; } TCB_t; 任务状态模型 RTOS中的任务通常有以下几种状态：\n+---------------+ | RUNNING | \u0026lt;------------------+ +-------+-------+ | | | 调度器选择该任务 | | | v ^ +-------+-------+ | | READY | | +---------------+ | ^ | | | +---------------+---------------+ | | | | | v v v | +-------------+ +-------------+ +-------------+ | | BLOCKED | | SUSPENDED | | DELETED |-------+ +-------------+ +-------------+ +-------------+ 状态详解 状态 说明 何时进入 Running 任务正在CPU上执行 调度器选中之 Ready 任务已就绪，等待CPU 创建后、解除阻塞后 Blocked 任务等待事件（信号量、队列、延时） 调用阻塞API Suspended 任务被暂停，不参与调度 调用vTaskSuspend() Deleted 任务被删除，资源待回收 调用vTaskDelete() 调度算法 1. 优先级抢占调度 RTOS最常用的调度算法。高优先级任务可以随时抢占低优先级任务。\n/* * 优先级抢占调度原理 * * 假设系统有3个任务： * - Task_High (优先级3) * - Task_Mid (优先级2) * - Task_Low (优先级1) * * 调度过程： * 1. Task_Low 运行中 * 2. Task_High 就绪 → 立即触发PendSV中断 * 3. PendSV保存Task_Low上下文 * 4. 加载Task_High上下文 * 5. Task_High 运行 * 6. Task_High 阻塞 → 恢复Task_Low */ FreeRTOS中的实现：\n// 任务调度核心 void vTaskSwitchContext(void) { if (uxSchedulerSuspended == pdFALSE) { traceMOVED_TASK_TO_READY_STATE(pxCurrentTCB); // 从就绪队列中选择最高优先级任务 taskSELECT_HIGHEST_PRIORITY_TASK(); traceTASK_SWITCHED_IN(); } } // 选择最高优先级任务（通用实现） void vApplicationTickHook(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 更新延时队列 xTaskIncrementTick(); // 触发调度 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } 优先级配置：\n// FreeRTOSConfig.h #define configMAX_PRIORITIES 5 // 最大优先级数量 #define configUSE_PREEMPTION 1 // 启用抢占式调度 #define configUSE_TIME_SLICING 1 // 启用时间片轮转 // 创建任务时指定优先级 (0 ~ configMAX_PRIORITIES-1) xTaskCreate( TaskFunction_t pvTaskCode, // 任务函数 const char *pcName, // 任务名 uint16_t usStackDepth, // 栈大小(字) void *pvParameters, // 参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t *pxCreatedTask // 任务句柄 ); 2. 时间片轮转调度 对于同优先级任务，采用时间片轮转方式调度，确保公平性。\n/* * 时间片轮转示意 * * Task_A (优先级2) - 时间片=10ms * Task_B (优先级2) - 时间片=10ms * * 时间线： * |----10ms----|----10ms----|----10ms----|----10ms----| * | Task_A | Task_B | Task_A | Task_B | * | | | | | * 0ms 10ms 20ms 30ms 40ms */ Tick中断中的处理：\nvoid xTaskIncrementTick(void) { TCB_t *pxTCB; // 遍历延时队列，递减计时 if (uxSchedulerSuspended == pdFALSE) { const TickType_t xConstTickCount = xTickCount; xTickCount++; // 检查是否有任务延时到期 if (xConstTickCount != 0) { taskRESET_ALL_TCB_AND_TCB_LISTS(); } // 时间片调度 #if configUSE_TIME_SLICING == 1 if (pxCurrentTCB-\u0026gt;time_slice \u0026gt; 0) { pxCurrentTCB-\u0026gt;time_slice--; } if (pxCurrentTCB-\u0026gt;time_slice == 0) { pxCurrentTCB-\u0026gt;time_slice = configTIME_SLICE_PERIOD; // 触发任务切换 portYIELD(); } #endif } } 调度触发时机 调度器在以下时机可能发生任务切换：\n触发时机 说明 系统节拍中断 任务时间片用完、延时到期 任务阻塞 高优先级任务阻塞，低优先级获得CPU 中断退出 ISR返回到高优先级就绪任务 任务创建/删除 发现更高优先级任务就绪 vTaskDelay 当前任务主动延时 /* * 典型的中断与调度配合 * * 硬件定时器中断 (10ms周期) */ void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 更新系统Tick xTaskIncrementTick(); // 检查是否有任务延时到期 if (xListLength(\u0026amp;xDelayedTaskList1) \u0026gt; 0) { TCB_t *pxTCB = (TCB_t *)xDelayedTaskList1.xListEnd.pvOwner; if (pxTCB-\u0026gt;xTicksToDelay == 0) { vListInsertEnd(\u0026amp;xReadyTasksLists[pxTCB-\u0026gt;uxPriority], \u0026amp;pxTCB-\u0026gt;xStateListItem); xHigherPriorityTaskWoken = pdTRUE; } } TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 触发PendSV进行任务切换 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } 优先级反转与解决 问题描述 优先级反转是一个经典的实时系统问题：\n时间 --\u0026gt; T1(高) ────────────────────────────────── T2(中) ───────────────── T3(低) |████████████████████████████| \u0026lt;--P3获得锁--\u0026gt; \u0026lt;--P3释放锁--\u0026gt; \u0026lt;--P1等待锁的时间--\u0026gt; ============== P1实际等待比P2更长！ 优先级继承 // 互斥锁实现优先级继承 typedef struct Mutex { Semaphore_t sem; TCB_t *owner; // 锁持有者 uint8_t inherited_priority; // 继承的优先级 } Mutex_t; void vTaskPriorityInherit(Mutex_t *pxMutex) { if (pxMutex-\u0026gt;owner-\u0026gt;priority \u0026lt; pxMutex-\u0026gt;owner-\u0026gt;BaseType) { // 提升持有者优先级 pxMutex-\u0026gt;inherited_priority = pxMutex-\u0026gt;owner-\u0026gt;BaseType; vTaskPrioritySet(pxMutex-\u0026gt;owner, pxMutex-\u0026gt;owner-\u0026gt;BaseType); } } void vTaskPriorityDisinherit(Mutex_t *pxMutex) { if (pxMutex-\u0026gt;owner != NULL) { // 恢复原优先级 vTaskPrioritySet(pxMutex-\u0026gt;owner, pxMutex-\u0026gt;inherited_priority); pxMutex-\u0026gt;owner = NULL; } } FreeRTOS中的二值信号量：\nSemaphoreHandle_t xMutex; void vCreateMutex(void) { xMutex = xSemaphoreCreateMutex(); } void vTaskA(void *param) { while (1) { if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) { // 临界区操作 vTaskDelay(pdMS_TO_TICKS(100)); xSemaphoreGive(xMutex); } vTaskDelay(pdMS_TO_TICKS(50)); } } 常见RTOS调度策略对比 RTOS 调度算法 优先级范围 时间片 抢占方式 FreeRTOS 固定优先级抢占 0-63 可配置 抢占+时间片 RT-Thread 优先级抢占+时间片 0-32 可配置 完全抢占 uCOS-II 优先级抢占 0-63 无 抢占 Azure RTOS 优先级抢占+轮转 0-28 可配置 完全抢占 实时性指标 评估RTOS实时性能的指标：\n1. 任务切换时间（Context Switch Time） /* * 上下文切换时间测量 */ volatile uint32_t ulSwitchTime[2]; void Task_Measure(void *param) { while (1) { ulSwitchTime[0] = DWT-\u0026gt;CYCCNT; // 开始计时 portYIELD(); // 触发切换 // 重新获得CPU时继续执行 ulSwitchTime[1] = DWT-\u0026gt;CYCCNT; // 结束计时 uint32_t cycles = ulSwitchTime[1] - ulSwitchTime[0]; float us = cycles / (SystemCoreClock / 1000000); printf(\u0026#34;Context switch: %lu cycles (%.2f us)\\n\u0026#34;, cycles, us); vTaskDelay(pdMS_TO_TICKS(1000)); } } 典型值：3-20微秒（取决于MCU架构）\n2. 中断延迟（Interrupt Latency） /* * 中断延迟 = 硬件中断响应时间 + 操作系统保存上下文时间 */ #define ISR_RESPONSE_TIME 5 // 硬件响应(典型值, 微秒) #define CONTEXT_SAVE_TIME 3 // 上下文保存(微秒) // 测量代码 void TIM_IRQHandler(void) { uint32_t isr_enter = DWT-\u0026gt;CYCCNT; // ... 中断处理 ... } 3. 调度延迟（Scheduling Latency） 从任务就绪到获得CPU的时间，取决于调度算法和系统负载。\n最佳实践 1. 合理分配优先级 /* * 优先级分配建议 * * 最高级 (0-7): 故障处理、紧急中断、安全关键 * 高级 (8-15): 通信处理、实时数据采集 * 中级 (16-31): 常规业务逻辑 * 低级 (32+): 日志记录、空闲任务 */ // 示例：工业控制系统的优先级分配 #define PRIORITY_CRITICAL 2 // 故障检测 #define PRIORITY_MOTOR_CTL 5 // 电机控制 (高实时性) #define PRIORITY_SENSOR 8 // 传感器采集 #define PRIORITY_UI 16 // UI显示 #define PRIORITY_LOGGING 32 // 日志记录 2. 避免常见错误 // 错误示例：在中断中调用非中断安全API void TIM_IRQHandler(void) { // 错误！vTaskDelay不是中断安全函数 vTaskDelay(pdMS_TO_TICKS(10)); // 正确：使用中断安全版本 BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(xSemaphore, \u0026amp;xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } 3. 使用互斥量保护共享资源 SemaphoreHandle_t xUARTMutex; void vUART_Init(void) { xUARTMutex = xSemaphoreCreateMutex(); } void vUART_SendString(const char *str) { xSemaphoreTake(xUARTMutex, portMAX_DELAY); { while (*str) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, *str++); } } xSemaphoreGive(xUARTMutex); } 4. 任务栈大小估算 /* * 任务栈需求估算 * * 需考虑： * - 函数调用嵌套深度 * - 局部变量大小 * - 中断嵌套深度 * - 浮点运算(如有) */ // 粗略估算 #define STACK_SIZE_LED_TASK 128 // 简单任务 #define STACK_SIZE_UART_TASK 256 // 有队列操作 #define STACK_SIZE_DISPLAY_TASK 384 // 较多局部变量 #define STACK_SIZE_MATH_TASK 512 // 浮点运算 // 检查栈使用 void vTaskMonitor(void *param) { while (1) { for (int i = 0; i \u0026lt; taskCOUNT; i++) { uint32_t used = uxTaskGetStackHighWaterMark(tasks[i].handle); printf(\u0026#34;Task %s: %lu words free\\n\u0026#34;, tasks[i].name, used); } vTaskDelay(pdMS_TO_TICKS(5000)); } } 总结 理解RTOS任务调度原理是嵌入式开发者的必备技能：\n任务状态模型：Ready/Blocked/Running 状态转换是基础 优先级抢占：高优先级任务可随时获得CPU 时间片轮转：保证同优先级任务的公平性 优先级反转：使用优先级继承或优先级天花板协议解决 合理设计：遵循优先级分配原则，避免常见错误 希望这篇详解能帮助你更好地理解RTOS调度机制，设计出更可靠的系统。\n","permalink":"/posts/rtos-task-scheduling/","summary":"\u003cp\u003e实时操作系统（RTOS）的核心价值在于提供确定性的响应时间和任务调度能力。作为嵌入式开发者，理解RTOS的任务调度原理对于合理设计任务优先级、避免优先级反转、优化系统性能至关重要。\u003c/p\u003e\n\u003ch2 id=\"任务与进程\"\u003e任务与进程\u003c/h2\u003e\n\u003cp\u003e在RTOS中，\u0026ldquo;任务\u0026rdquo;（Task）通常对应一个执行流。与传统操作系统的进程不同：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e特性\u003c/th\u003e\n          \u003cth\u003e进程\u003c/th\u003e\n          \u003cth\u003eRTOS任务\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e地址空间\u003c/td\u003e\n          \u003ctd\u003e隔离\u003c/td\u003e\n          \u003ctd\u003e共享\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e栈空间\u003c/td\u003e\n          \u003ctd\u003e独立\u003c/td\u003e\n          \u003ctd\u003e独立\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e资源隔离\u003c/td\u003e\n          \u003ctd\u003e强\u003c/td\u003e\n          \u003ctd\u003e弱\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e创建/切换开销\u003c/td\u003e\n          \u003ctd\u003e大\u003c/td\u003e\n          \u003ctd\u003e小\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRTOS任务更像是轻量级的线程，共享相同的地址空间，但拥有独立的栈和上下文。\u003c/p\u003e\n\u003ch2 id=\"任务控制块tcb\"\u003e任务控制块（TCB）\u003c/h2\u003e\n\u003cp\u003e任务控制块（TCB - Task Control Block）是RTOS管理任务的核心数据结构：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003etypedef\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003estruct\u003c/span\u003e tcb {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003evolatile\u003c/span\u003e \u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003esp;              \u003cspan style=\"color:#586e75\"\u003e// 栈指针\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    \u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e priority;               \u003cspan style=\"color:#586e75\"\u003e// 任务优先级\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    \u003cspan style=\"color:#dc322f\"\u003euint16_t\u003c/span\u003e stack_size;            \u003cspan style=\"color:#586e75\"\u003e// 栈大小\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    StackType_t \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003estack_base;        \u003cspan style=\"color:#586e75\"\u003e// 栈底\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#586e75\"\u003e// 状态信息\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    TaskState state;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e time_slice;            \u003cspan style=\"color:#586e75\"\u003e// 时间片计数\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    \u003cspan style=\"color:#dc322f\"\u003euint32_t\u003c/span\u003e tick_count;           \u003cspan style=\"color:#586e75\"\u003e// 滴答计数\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#586e75\"\u003e// 链表节点（用于就绪队列/阻塞队列）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    ListItem_t state_list_item;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListItem_t event_list_item;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#586e75\"\u003e// 任务函数\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    TaskFunction_t func;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eparam;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#586e75\"\u003e// 任务名（用于调试）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    \u003cspan style=\"color:#dc322f\"\u003echar\u003c/span\u003e name[configMAX_TASK_NAME_LEN];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e} TCB_t;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"任务状态模型\"\u003e任务状态模型\u003c/h2\u003e\n\u003cp\u003eRTOS中的任务通常有以下几种状态：\u003c/p\u003e","title":"深入理解RTOS任务调度原理"},{"content":"状态机（FSM - Finite State Machine）是嵌入式开发中最常用的设计模式之一。它能够帮助开发者将复杂的逻辑分解为清晰的状态和转换，特别适合处理按键检测、协议解析、模式切换等场景。\n什么是状态机？ 状态机是一种抽象的计算模型，由以下要素组成：\n要素 说明 状态 (State) 系统在某一时刻的稳定条件或工作模式 事件 (Event) 触发状态转换的外部信号或内部条件 转换 (Transition) 状态之间的有向转移 动作 (Action) 状态转换时执行的操作 状态转移图 状态机可以用状态转移图直观地表示：\n+-----------------+ | | | IDLE |\u0026lt;---------------+ | | | +--------+--------+ | | | 事件:EV_KEY_PRESS | | | v | +--------+--------+ | | | | | PRESSED |-------+ | | | | | +-----------------+ | | | | 定时器超时 | | 按键释放 (消抖完成) | | | | v | +-----------------+ +--------+------\u0026gt;+ | | | | RELEASED |----------------------\u0026gt;+ | | 消抖完成，进入IDLE +-----------------+ 核心要素详解 状态定义 typedef enum { STATE_IDLE, // 空闲状态 STATE_PRESSED, // 按键按下（消抖中） STATE_ACTIVE, // 按键确认按下 STATE_RELEASED, // 按键释放（消抖中） } KeyState; 事件定义 typedef enum { EV_KEY_PRESS, // 按键按下 EV_KEY_RELEASE, // 按键释放 EV_TIMEOUT, // 定时器超时 } KeyEvent; 状态表 typedef struct { KeyState current_state; KeyEvent event; KeyState next_state; void (*action)(void); // 转换时执行的动作 } StateTransition; // 状态转换表 static const StateTransition g_trans_table[] = { {STATE_IDLE, EV_KEY_PRESS, STATE_PRESSED, Start_DebounceTimer}, {STATE_PRESSED, EV_TIMEOUT, STATE_ACTIVE, On_KeyConfirmed}, {STATE_ACTIVE, EV_KEY_RELEASE,STATE_RELEASED, Start_DebounceTimer}, {STATE_RELEASED, EV_TIMEOUT, STATE_IDLE, On_KeyReleased}, }; 实现方式 1. Switch-Case 实现 最简单直观的方式：\ntypedef enum { STATE_IDLE, STATE_HEATING, STATE_KEEPING, STATE_COOLING } WaterHeaterState; static WaterHeaterState g_state = STATE_IDLE; static uint32_t g_temperature = 25; static uint32_t g_target_temp = 60; void WaterHeater_Process(void) { switch (g_state) { case STATE_IDLE: g_temperature = 25; if (g_target_temp \u0026gt; 25) { g_state = STATE_HEATING; Heater_On(); } break; case STATE_HEATING: if (g_temperature \u0026gt;= g_target_temp) { g_state = STATE_KEEPING; Heater_Off(); } break; case STATE_KEEPING: if (g_temperature \u0026lt; g_target_temp - 5) { g_state = STATE_HEATING; Heater_On(); } else if (g_target_temp == 0) { g_state = STATE_COOLING; Heater_Off(); } break; case STATE_COOLING: if (g_temperature \u0026lt;= 30) { g_state = STATE_IDLE; } break; default: g_state = STATE_IDLE; break; } } // 定时器中断中更新温度和处理超时 void Timer5ms_IRQHandler(void) { static uint8_t counter = 0; counter++; if (g_state == STATE_HEATING) { g_temperature += 2; // 每5ms升温2度 } else if (g_state == STATE_COOLING) { g_temperature -= 1; // 自然冷却 } // 某些状态需要超时检测 if (counter \u0026gt;= 200) { // 1秒 counter = 0; On_Timeout(); } } 优点：\n逻辑清晰，易于理解 调试方便，可以单步跟踪状态变化 适合状态较少的场景 缺点：\n状态和事件增多时，代码膨胀快 状态转换逻辑分散，不易维护 不易扩展新的状态转换 2. 状态表驱动实现 将状态转换关系数据化，更适合复杂状态机：\n#include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; // 状态和事件定义 typedef enum { ST_IDLE = 0, ST_CONNECTING, ST_CONNECTED, ST_DISCONNECTING, ST_ERROR, ST_MAX } ConnState; typedef enum { EV_START, EV_CONNECTED, EV_DISCONNECT, EV_TIMEOUT, EV_ERROR, EV_MAX } ConnEvent; // 状态转换表 typedef struct { ConnState next_state; void (*action)(void *param); } Transition; static const Transition g_fsm[ST_MAX][EV_MAX] = { // ST_IDLE { [EV_START] = {ST_CONNECTING, do_connect}, [EV_ERROR] = {ST_ERROR, do_error_handler}, }, // ST_CONNECTING { [EV_CONNECTED] = {ST_CONNECTED, do_on_connected}, [EV_TIMEOUT] = {ST_ERROR, do_timeout_handler}, [EV_ERROR] = {ST_ERROR, do_error_handler}, }, // ST_CONNECTED { [EV_DISCONNECT]= {ST_DISCONNECTING, do_disconnect}, [EV_ERROR] = {ST_ERROR, do_error_handler}, }, // ST_DISCONNECTING { [EV_CONNECTED] = {ST_IDLE, do_cleanup}, // 完成清理 [EV_TIMEOUT] = {ST_IDLE, do_cleanup}, }, // ST_ERROR { [EV_START] = {ST_IDLE, do_reset}, }, }; // 状态机上下文 typedef struct { ConnState state; void *user_data; uint32_t error_count; } FSM_Context; static FSM_Context g_fsm_ctx = {.state = ST_IDLE, .error_count = 0}; // 状态机执行函数 bool FSM_HandleEvent(ConnEvent event, void *param) { if (g_fsm_ctx.state \u0026gt;= ST_MAX || event \u0026gt;= EV_MAX) { return false; } const Transition *t = \u0026amp;g_fsm[g_fsm_ctx.state][event]; if (t-\u0026gt;action) { t-\u0026gt;action(param); } if (t-\u0026gt;next_state != g_fsm_ctx.state) { g_fsm_ctx.state = t-\u0026gt;next_state; return true; } return false; } 优点：\n状态转换逻辑集中，易于维护 新增状态和转换只需修改表数据 适合复杂状态机 便于自动化生成 缺点：\n需要额外的存储空间 调试时不易查看状态流程 初始设计和表构建需要更多精力 3. 层次状态机（HSM） 对于复杂系统，可以使用层次状态机，将相关状态分组：\ntypedef struct StateNode { const char *name; struct StateNode *parent; // 父状态 struct StateNode *initial; // 初始子状态 struct StateNode *active; // 当前活跃子状态 } StateNode; // 定义状态层次结构 static StateNode state_running = { .name = \u0026#34;RUNNING\u0026#34;, }; static StateNode state_error = { .name = \u0026#34;ERROR\u0026#34;, }; static StateNode state_machine = { .name = \u0026#34;ROOT\u0026#34;, .parent = NULL, .initial = \u0026amp;state_running, }; // 状态进入/退出动作 void StateMachine_Enter(StateNode *state) { printf(\u0026#34;Entering: %s\\n\u0026#34;, state-\u0026gt;name); if (state-\u0026gt;initial) { state-\u0026gt;active = state-\u0026gt;initial; StateMachine_Enter(state-\u0026gt;active); } } void StateMachine_Exit(StateNode *state) { if (state-\u0026gt;active) { StateMachine_Exit(state-\u0026gt;active); } printf(\u0026#34;Exiting: %s\\n\u0026#34;, state-\u0026gt;name); } void StateMachine_Transition(StateNode *target) { // 计算LCA，退出并重新进入 StateMachine_Exit(\u0026amp;state_machine); state_machine.active = target; StateMachine_Enter(\u0026amp;state_machine); } 实际应用案例 案例1：按键消抖处理 typedef enum { KEY_STATE_IDLE, KEY_STATE_DEBOUNCE, KEY_STATE_PRESSED, KEY_STATE_LONGPRESS, KEY_STATE_RELEASE } KeyState; typedef struct { KeyState state; uint32_t press_time; uint8_t key_id; } KeyContext; #define LONGPRESS_TIME_MS 1000 #define DEBOUNCE_TIME_MS 20 void KeyFSM_Init(KeyContext *ctx) { ctx-\u0026gt;state = KEY_STATE_IDLE; ctx-\u0026gt;press_time = 0; } void KeyFSM_Event(KeyContext *ctx, uint8_t event, uint32_t now) { switch (ctx-\u0026gt;state) { case KEY_STATE_IDLE: if (event == KEY_PRESS) { ctx-\u0026gt;press_time = now; ctx-\u0026gt;state = KEY_STATE_DEBOUNCE; } break; case KEY_STATE_DEBOUNCE: if (event == KEY_RELEASE) { ctx-\u0026gt;state = KEY_STATE_IDLE; } else if (event == KEY_TIMEOUT \u0026amp;\u0026amp; (now - ctx-\u0026gt;press_time) \u0026gt;= DEBOUNCE_TIME_MS) { ctx-\u0026gt;state = KEY_STATE_PRESSED; On_KeyPressed(ctx-\u0026gt;key_id); } break; case KEY_STATE_PRESSED: if (event == KEY_RELEASE) { ctx-\u0026gt;state = KEY_STATE_RELEASE; } else if (event == KEY_TIMEOUT \u0026amp;\u0026amp; (now - ctx-\u0026gt;press_time) \u0026gt;= LONGPRESS_TIME_MS) { ctx-\u0026gt;state = KEY_STATE_LONGPRESS; On_KeyLongPressed(ctx-\u0026gt;key_id); } break; case KEY_STATE_LONGPRESS: if (event == KEY_RELEASE) { ctx-\u0026gt;state = KEY_STATE_RELEASE; } break; case KEY_STATE_RELEASE: if (event == KEY_TIMEOUT) { On_KeyReleased(ctx-\u0026gt;key_id); ctx-\u0026gt;state = KEY_STATE_IDLE; } break; } } 案例2：菜单系统 typedef enum { MENU_MAIN, MENU_SETTINGS, MENU_ABOUT, MENU_SETTINGS_DISPLAY, MENU_SETTINGS_SOUND } MenuState; typedef struct { MenuState state; uint8_t selection; } MenuContext; static const char *menu_items[][4] = { [MENU_MAIN] = {\u0026#34;Start\u0026#34;, \u0026#34;Settings\u0026#34;, \u0026#34;About\u0026#34;, NULL}, [MENU_SETTINGS] = {\u0026#34;Display\u0026#34;, \u0026#34;Sound\u0026#34;, \u0026#34;Back\u0026#34;, NULL}, [MENU_SETTINGS_DISPLAY] = {\u0026#34;Brightness\u0026#34;, \u0026#34;Timeout\u0026#34;, \u0026#34;Back\u0026#34;, NULL}, [MENU_SETTINGS_SOUND] = {\u0026#34;Volume\u0026#34;, \u0026#34;KeySound\u0026#34;, \u0026#34;Back\u0026#34;, NULL}, [MENU_ABOUT] = {\u0026#34;Version 1.0\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Back\u0026#34;, NULL}, }; void Menu_HandleKey(MenuContext *ctx, uint8_t key) { switch (key) { case KEY_UP: if (ctx-\u0026gt;selection \u0026gt; 0) ctx-\u0026gt;selection--; break; case KEY_DOWN: if (menu_items[ctx-\u0026gt;state][ctx-\u0026gt;selection + 1] != NULL) { ctx-\u0026gt;selection++; } break; case KEY_ENTER: Menu_Navigate(ctx); break; case KEY_BACK: Menu_Back(ctx); break; } Menu_UpdateDisplay(ctx); } void Menu_Navigate(MenuContext *ctx) { switch (ctx-\u0026gt;state) { case MENU_MAIN: if (ctx-\u0026gt;selection == 0) StartProcess(); else if (ctx-\u0026gt;selection == 1) ctx-\u0026gt;state = MENU_SETTINGS; else if (ctx-\u0026gt;selection == 2) ctx-\u0026gt;state = MENU_ABOUT; ctx-\u0026gt;selection = 0; break; // ... 其他状态处理 } } 设计建议 1. 状态定义要完整无遗漏 // 好的实践：使用枚举确保所有状态都被处理 typedef enum { #define STATE(name) name, #include \u0026#34;states.def\u0026#34; #undef STATE STATE_COUNT } State; 2. 处理所有可能的转换 使用断言或默认分支处理非法转换：\nswitch (state) { case STATE_A: ...; break; case STATE_B: ...; break; default: ASSERT(0, \u0026#34;Unexpected state transition\u0026#34;); } 3. 进入和退出动作 为每个状态定义清楚的生命周期：\nvoid State_Enter(State *s) { // 初始化状态资源 } void State_Exit(State *s) { // 清理状态资源 } 4. 状态机文档化 编写清晰的状态转换文档：\n/* * 状态机: UART通信管理 * * 状态: * IDLE - 空闲，等待发送或接收 * SENDING - 正在发送数据 * RECEIVING - 正在接收数据 * ERROR - 通信错误 * * 转换: * IDLE + send_req -\u0026gt; SENDING * IDLE + recv_data -\u0026gt; RECEIVING * SENDING + complete -\u0026gt; IDLE * ANY + error -\u0026gt; ERROR * ERROR + reset -\u0026gt; IDLE */ 调试技巧 日志输出：状态转换时输出日志 断点调试：在状态切换处设置断点 可视化：使用状态机可视化工具 测试用例：为每个状态转换编写测试 总结 状态机是嵌入式开发中不可或缺的利器：\n将复杂逻辑分解为清晰的状态和转换 提高代码可维护性和可测试性 多种实现方式，适用于不同复杂度场景 结合设计文档，获得更好的开发体验 掌握状态机设计，让你的嵌入式代码更加专业可靠。\n","permalink":"/posts/state-machine-embedded/","summary":"\u003cp\u003e状态机（FSM - Finite State Machine）是嵌入式开发中最常用的设计模式之一。它能够帮助开发者将复杂的逻辑分解为清晰的状态和转换，特别适合处理按键检测、协议解析、模式切换等场景。\u003c/p\u003e\n\u003ch2 id=\"什么是状态机\"\u003e什么是状态机？\u003c/h2\u003e\n\u003cp\u003e状态机是一种抽象的计算模型，由以下要素组成：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e要素\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e状态 (State)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e系统在某一时刻的稳定条件或工作模式\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e事件 (Event)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e触发状态转换的外部信号或内部条件\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e转换 (Transition)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e状态之间的有向转移\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e动作 (Action)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e状态转换时执行的操作\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"状态转移图\"\u003e状态转移图\u003c/h2\u003e\n\u003cp\u003e状态机可以用状态转移图直观地表示：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    +-----------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |                 |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |    IDLE         |\u0026lt;---------------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |                 |                |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    +--------+--------+                 |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                             |                          |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    事件:EV_KEY_PRESS                   |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                             |                          |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                             v                          |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    +--------+--------+                |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |                 |                |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |   PRESSED       |-------+        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |                 |       |        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    +-----------------+        |        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                              |        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                              定时器超时      |        | 按键释放\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                              (消抖完成)      |        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                              |        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                              v        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    +-----------------+        +--------+------\u0026gt;+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |                 |                        |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |   RELEASED      |----------------------\u0026gt;+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    |                 |     消抖完成，进入IDLE\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    +-----------------+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"核心要素详解\"\u003e核心要素详解\u003c/h2\u003e\n\u003ch3 id=\"状态定义\"\u003e状态定义\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003etypedef\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003eenum\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    STATE_IDLE,       \u003cspan style=\"color:#586e75\"\u003e// 空闲状态\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    STATE_PRESSED,    \u003cspan style=\"color:#586e75\"\u003e// 按键按下（消抖中）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    STATE_ACTIVE,     \u003cspan style=\"color:#586e75\"\u003e// 按键确认按下\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    STATE_RELEASED,   \u003cspan style=\"color:#586e75\"\u003e// 按键释放（消抖中）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e} KeyState;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"事件定义\"\u003e事件定义\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003etypedef\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003eenum\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    EV_KEY_PRESS,      \u003cspan style=\"color:#586e75\"\u003e// 按键按下\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    EV_KEY_RELEASE,    \u003cspan style=\"color:#586e75\"\u003e// 按键释放\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    EV_TIMEOUT,        \u003cspan style=\"color:#586e75\"\u003e// 定时器超时\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e} KeyEvent;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"状态表\"\u003e状态表\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#719e07\"\u003etypedef\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003estruct\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    KeyState current_state;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    KeyEvent event;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    KeyState next_state;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e (\u003cspan style=\"color:#719e07\"\u003e*\u003c/span\u003eaction)(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e);  \u003cspan style=\"color:#586e75\"\u003e// 转换时执行的动作\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e} StateTransition;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 状态转换表\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#719e07\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#719e07\"\u003econst\u003c/span\u003e StateTransition g_trans_table[] \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {STATE_IDLE,      EV_KEY_PRESS,  STATE_PRESSED,  Start_DebounceTimer},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {STATE_PRESSED,   EV_TIMEOUT,    STATE_ACTIVE,   On_KeyConfirmed},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {STATE_ACTIVE,    EV_KEY_RELEASE,STATE_RELEASED, Start_DebounceTimer},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {STATE_RELEASED,  EV_TIMEOUT,    STATE_IDLE,     On_KeyReleased},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"实现方式\"\u003e实现方式\u003c/h2\u003e\n\u003ch3 id=\"1-switch-case-实现\"\u003e1. Switch-Case 实现\u003c/h3\u003e\n\u003cp\u003e最简单直观的方式：\u003c/p\u003e","title":"状态机在嵌入式开发中的应用"},{"content":"在嵌入式开发中，良好的架构设计是项目可维护性和可扩展性的基础。没有操作系统的情况下，如何组织代码结构显得尤为重要。本文将介绍几种常见的裸机架构模式，帮助你选择适合自己项目的方案。\n常见的裸机架构模式 1. 超级循环（Super Loop） 超级循环是最简单也最直接的架构模式。主循环依次调用各任务处理函数，顺序执行所有业务逻辑。\n代码示例：\nint main(void) { System_Init(); while (1) { Task_KeyScan(); // 按键扫描 Task_Display(); // 显示屏刷新 Task_Sensor(); // 传感器采集 Task_Communication(); // 通信处理 // 可选：加入延时控制循环频率 delay_ms(10); } } 优点：\n代码结构简单，易于理解和实现 无额外资源消耗，无需调度器 中断响应确定性好 缺点：\n任务耦合严重，修改一个任务可能影响其他任务 实时性差，所有任务必须等待循环执行 任务数量和复杂度受循环时间限制 适用场景： 任务简单、实时性要求不高的简单产品，如玩具、小家电控制板。\n2. 前后台系统（Foreground-Background） 将紧急任务放在中断前台处理，非紧急任务在主循环后台执行。这是最常用的裸机架构模式。\n代码示例：\n// 前台：中断服务程序 void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // 紧急事件处理，如按键按下、外部报警 g_key_pressed = 1; g_system_error = ERROR_NONE; EXTI_ClearITPendingBit(EXTI_Line0); } } void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { // 定时器中断，标记时间基准 g_timer_flag = 1; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } // 后台：主循环 int main(void) { System_Init(); while (1) { if (g_timer_flag) { g_timer_flag = 0; Task_10ms(); // 10ms周期任务 } if (g_key_pressed) { g_key_pressed = 0; Task_KeyProcess(); // 按键处理 } Task_Display(); // 显示屏刷新 Task_Communication(); // 通信处理 } } 优点：\n紧急事件由中断快速响应，保证实时性 结构清晰，任务分层明确 资源消耗小 缺点：\n中断中不宜执行耗时操作 后台任务仍受循环执行影响 中断优先级管理需要谨慎设计 适用场景： 大多数中低端嵌入式产品，如家用电器、工业控制模块。\n3. 事件驱动架构（Event-Driven） 基于事件队列的架构模式，实现任务间解耦。通过事件标志或消息队列将事件与处理分离。\n代码示例：\n// 事件定义 typedef enum { EVENT_KEY_PRESS, EVENT_UART_RECV, EVENT_TIMER, EVENT_SENSOR_DATA_READY, EVENT_MAX } Event_Type; typedef struct { Event_Type type; uint32_t param; } Event; // 事件队列 static Event g_event_queue[MAX_EVENT_QUEUE]; static uint8_t g_event_head = 0; static uint8_t g_event_tail = 0; static void Event_Push(Event_Type type, uint32_t param) { uint8_t next = (g_event_head + 1) % MAX_EVENT_QUEUE; if (next != g_event_tail) { g_event_queue[g_event_head].type = type; g_event_queue[g_event_head].param = param; g_event_head = next; } } static Event* Event_Pop(void) { if (g_event_head != g_event_tail) { uint8_t current = g_event_tail; g_event_tail = (g_event_tail + 1) % MAX_EVENT_QUEUE; return \u0026amp;g_event_queue[current]; } return NULL; } // 事件处理表 typedef void (*EventHandler)(uint32_t param); static EventHandler g_handlers[EVENT_MAX]; void Event_Init(void) { g_handlers[EVENT_KEY_PRESS] = On_KeyPress; g_handlers[EVENT_UART_RECV] = On_UartRecv; g_handlers[EVENT_TIMER] = On_Timer; g_handlers[EVENT_SENSOR_DATA_READY] = On_SensorReady; } int main(void) { System_Init(); Event_Init(); while (1) { Event *evt = Event_Pop(); if (evt \u0026amp;\u0026amp; g_handlers[evt-\u0026gt;type]) { g_handlers[evt-\u0026gt;type](evt-\u0026gt;param); } } } // 中断中产生事件 void UART1_IRQHandler(void) { if (USART_GetITStatus(UART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(UART1); Event_Push(EVENT_UART_RECV, data); } } 优点：\n任务间松耦合，易于维护和扩展 新增功能只需添加事件处理函数 便于模块化测试 缺点：\n需要额外的内存管理 事件处理延迟不确定 系统复杂度随事件增多而增加 适用场景： 功能较多、需要灵活扩展的中等复杂度产品。\n4. 时间片轮询（Time-Triggered Polling） 基于系统滴答定时器的时间片调度，将CPU时间划分为固定长度的时间片，每个任务占用一个时间片。\n代码示例：\n#define TASK_COUNT 4 #define TICK_MS 10 #define TASK0_PERIOD 1 // 10ms #define TASK1_PERIOD 2 // 20ms #define TASK2_PERIOD 5 // 50ms #define TASK3_PERIOD 10 // 100ms typedef struct { void (*func)(void); uint16_t period; // 周期（Tick数） uint16_t elapsed; // 已用时间 } TaskDef; static TaskDef g_tasks[TASK_COUNT] = { {Task_LED, TASK0_PERIOD, 0}, {Task_KeyScan, TASK1_PERIOD, 0}, {Task_Display, TASK2_PERIOD, 0}, {Task_Communication, TASK3_PERIOD, 0} }; void Scheduler_Init(void) { Systick_Config(TICK_MS); // 配置SysTick为10ms中断 } void Scheduler_Update(void) { for (int i = 0; i \u0026lt; TASK_COUNT; i++) { g_tasks[i].elapsed++; if (g_tasks[i].elapsed \u0026gt;= g_tasks[i].period) { g_tasks[i].elapsed = 0; g_tasks[i].func(); } } } void SysTick_Handler(void) { Scheduler_Update(); } int main(void) { System_Init(); Scheduler_Init(); while (1) { // 主循环可以处理非实时任务 Task_PowerManagement(); } } 优点：\n任务调度确定性好，实时性有保障 CPU时间公平分配，避免单个任务饥饿 易于添加和删除任务 缺点：\n时间片大小选择需要权衡 实时性受任务执行时间影响 不适合异步事件处理 适用场景： 多任务定时执行、实时性要求较高的产品。\n架构选择指南 架构 复杂度 实时性 扩展性 适用场景 超级循环 低 差 差 简单产品、任务极少 前后台 中 良 中 大多数中低端产品 事件驱动 中高 中 良 功能较多的复杂产品 时间片轮询 中 优 中 多任务实时系统 选择建议：\n任务简单（3个以内）：直接用超级循环，简单高效 有紧急事件处理：选择前后台系统 任务较多、需要解耦：事件驱动架构 多任务实时系统：时间片轮询调度 最佳实践 1. 模块化设计 将系统拆分为独立的功能模块，每个模块：\n有明确的职责边界 通过接口与其他模块交互 可以独立编译和测试 // 模块头文件示例 #ifndef __BSP_LED_H__ #define __BSP_LED_H__ void LED_Init(void); void LED_On(uint8_t led); void LED_Off(uint8_t led); void LED_Toggle(uint8_t led); #endif 2. 分层思想 +-------------------+ | Application | 应用层：业务逻辑 +-------------------+ | Middleware | 中间层：协议、算法 +-------------------+ | Driver | 驱动层：外设控制 +-------------------+ | Hardware | 硬件层：MCU、电路 +-------------------+ 3. 状态机管理 在模块内部使用状态机管理复杂逻辑，详见另一篇文章《状态机在嵌入式开发中的应用》。\n4. 配置分离 将可配置参数集中管理，便于产品定制和维护：\n// config.h #ifndef __CONFIG_H__ #define __CONFIG_H__ #define LED_BLINK_INTERVAL_MS 500 #define KEY_SCAN_INTERVAL_MS 20 #define UART_BAUDRATE 115200 #define SYSTEM_CLOCK 72000000 #endif 总结 裸机架构设计没有标准答案，需要根据具体项目需求选择合适的模式。关键原则是：\n够用就好：不要过度设计，简单项目用简单架构 预留扩展：考虑未来可能的需求变化 保持一致：同一项目内保持架构风格统一 注重可维护性：代码是写给人看的，易于维护最重要 在实际项目中，常常会混合使用多种架构模式，根据各模块的特点选择最合适的方式。\n","permalink":"/posts/bare-metal-architecture/","summary":"\u003cp\u003e在嵌入式开发中，良好的架构设计是项目可维护性和可扩展性的基础。没有操作系统的情况下，如何组织代码结构显得尤为重要。本文将介绍几种常见的裸机架构模式，帮助你选择适合自己项目的方案。\u003c/p\u003e\n\u003ch2 id=\"常见的裸机架构模式\"\u003e常见的裸机架构模式\u003c/h2\u003e\n\u003ch3 id=\"1-超级循环super-loop\"\u003e1. 超级循环（Super Loop）\u003c/h3\u003e\n\u003cp\u003e超级循环是最简单也最直接的架构模式。主循环依次调用各任务处理函数，顺序执行所有业务逻辑。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代码示例：\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003emain\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#268bd2\"\u003eSystem_Init\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003ewhile\u003c/span\u003e (\u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eTask_KeyScan\u003c/span\u003e();      \u003cspan style=\"color:#586e75\"\u003e// 按键扫描\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#268bd2\"\u003eTask_Display\u003c/span\u003e();       \u003cspan style=\"color:#586e75\"\u003e// 显示屏刷新\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#268bd2\"\u003eTask_Sensor\u003c/span\u003e();       \u003cspan style=\"color:#586e75\"\u003e// 传感器采集\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#268bd2\"\u003eTask_Communication\u003c/span\u003e(); \u003cspan style=\"color:#586e75\"\u003e// 通信处理\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#586e75\"\u003e// 可选：加入延时控制循环频率\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#268bd2\"\u003edelay_ms\u003c/span\u003e(\u003cspan style=\"color:#2aa198\"\u003e10\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e优点：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e代码结构简单，易于理解和实现\u003c/li\u003e\n\u003cli\u003e无额外资源消耗，无需调度器\u003c/li\u003e\n\u003cli\u003e中断响应确定性好\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e缺点：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e任务耦合严重，修改一个任务可能影响其他任务\u003c/li\u003e\n\u003cli\u003e实时性差，所有任务必须等待循环执行\u003c/li\u003e\n\u003cli\u003e任务数量和复杂度受循环时间限制\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e适用场景：\u003c/strong\u003e 任务简单、实时性要求不高的简单产品，如玩具、小家电控制板。\u003c/p\u003e\n\u003ch3 id=\"2-前后台系统foreground-background\"\u003e2. 前后台系统（Foreground-Background）\u003c/h3\u003e\n\u003cp\u003e将紧急任务放在中断前台处理，非紧急任务在主循环后台执行。这是最常用的裸机架构模式。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代码示例：\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 前台：中断服务程序\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eEXTI0_IRQHandler\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#268bd2\"\u003eEXTI_GetITStatus\u003c/span\u003e(EXTI_Line0) \u003cspan style=\"color:#719e07\"\u003e!=\u003c/span\u003e RESET) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#586e75\"\u003e// 紧急事件处理，如按键按下、外部报警\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        g_key_pressed \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        g_system_error \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e ERROR_NONE;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eEXTI_ClearITPendingBit\u003c/span\u003e(EXTI_Line0);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003eTIM2_IRQHandler\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#268bd2\"\u003eTIM_GetITStatus\u003c/span\u003e(TIM2, TIM_IT_Update) \u003cspan style=\"color:#719e07\"\u003e!=\u003c/span\u003e RESET) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#586e75\"\u003e// 定时器中断，标记时间基准\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        g_timer_flag \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eTIM_ClearITPendingBit\u003c/span\u003e(TIM2, TIM_IT_Update);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e// 后台：主循环\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e\u003cspan style=\"color:#dc322f\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#268bd2\"\u003emain\u003c/span\u003e(\u003cspan style=\"color:#dc322f\"\u003evoid\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#268bd2\"\u003eSystem_Init\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#719e07\"\u003ewhile\u003c/span\u003e (\u003cspan style=\"color:#2aa198\"\u003e1\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#719e07\"\u003eif\u003c/span\u003e (g_timer_flag) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            g_timer_flag \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#268bd2\"\u003eTask_10ms\u003c/span\u003e();   \u003cspan style=\"color:#586e75\"\u003e// 10ms周期任务\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#719e07\"\u003eif\u003c/span\u003e (g_key_pressed) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            g_key_pressed \u003cspan style=\"color:#719e07\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#2aa198\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#268bd2\"\u003eTask_KeyProcess\u003c/span\u003e();  \u003cspan style=\"color:#586e75\"\u003e// 按键处理\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#268bd2\"\u003eTask_Display\u003c/span\u003e();       \u003cspan style=\"color:#586e75\"\u003e// 显示屏刷新\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#268bd2\"\u003eTask_Communication\u003c/span\u003e(); \u003cspan style=\"color:#586e75\"\u003e// 通信处理\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#586e75\"\u003e\u003c/span\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e优点：\u003c/strong\u003e\u003c/p\u003e","title":"嵌入式裸机程序架构设计"},{"content":"你好，欢迎来到我的技术博客。\n作为一名嵌入式软件工程师，我创建这个博客是为了记录学习过程、分享开发经验，并与同行交流技术心得。\n关于我 我目前专注于嵌入式软件领域，主要涉及以下几个方向：\n裸机程序开发：在资源受限的单片机平台上实现高效、可靠的业务逻辑 RTOS应用开发：基于FreeRTOS、RT-Thread等实时操作系统构建复杂应用 驱动程序开发：GPIO、UART、SPI、I2C、ADC等外设驱动的设计与实现 系统优化：提升系统实时性、降低功耗、优化内存使用 技术栈 我的主要开发环境和技术工具：\n分类 技术/工具 MCU STM32、NXP LPC、Cypress PSoC RTOS FreeRTOS、RT-Thread 调试 J-Link、ST-Link、Oscilloscope IDE Keil MDK、IAR EWARM、VS Code 语言 C、嵌入式汇编、Python脚本 博客内容规划 本博客将持续更新以下几个方向的内容：\n嵌入式开发实战：结合实际项目，分享开发经验和踩坑记录 RTOS内核解析：深入剖析实时操作系统的核心机制 驱动开发笔记：各类外设驱动的设计思路与实现细节 工具链使用：编译器、调试器、自动化工具的使用技巧 为什么写博客？ 知识沉淀：写下来能加深理解，也方便日后回顾 技术交流：希望能与同行交流，互相学习 社区贡献：把遇到的问题和解决方案分享出去，帮助有需要的人 联系我 如果你有任何问题或建议，欢迎通过以下方式与我交流：\nGitHub: 我的GitHub 希望我的文章能对你有所帮助！\n","permalink":"/posts/welcome/","summary":"\u003cp\u003e你好，欢迎来到我的技术博客。\u003c/p\u003e\n\u003cp\u003e作为一名嵌入式软件工程师，我创建这个博客是为了记录学习过程、分享开发经验，并与同行交流技术心得。\u003c/p\u003e\n\u003ch2 id=\"关于我\"\u003e关于我\u003c/h2\u003e\n\u003cp\u003e我目前专注于嵌入式软件领域，主要涉及以下几个方向：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e裸机程序开发\u003c/strong\u003e：在资源受限的单片机平台上实现高效、可靠的业务逻辑\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRTOS应用开发\u003c/strong\u003e：基于FreeRTOS、RT-Thread等实时操作系统构建复杂应用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e驱动程序开发\u003c/strong\u003e：GPIO、UART、SPI、I2C、ADC等外设驱动的设计与实现\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e系统优化\u003c/strong\u003e：提升系统实时性、降低功耗、优化内存使用\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"技术栈\"\u003e技术栈\u003c/h2\u003e\n\u003cp\u003e我的主要开发环境和技术工具：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e分类\u003c/th\u003e\n          \u003cth\u003e技术/工具\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMCU\u003c/td\u003e\n          \u003ctd\u003eSTM32、NXP LPC、Cypress PSoC\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eRTOS\u003c/td\u003e\n          \u003ctd\u003eFreeRTOS、RT-Thread\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e调试\u003c/td\u003e\n          \u003ctd\u003eJ-Link、ST-Link、Oscilloscope\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eIDE\u003c/td\u003e\n          \u003ctd\u003eKeil MDK、IAR EWARM、VS Code\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e语言\u003c/td\u003e\n          \u003ctd\u003eC、嵌入式汇编、Python脚本\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"博客内容规划\"\u003e博客内容规划\u003c/h2\u003e\n\u003cp\u003e本博客将持续更新以下几个方向的内容：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e嵌入式开发实战\u003c/strong\u003e：结合实际项目，分享开发经验和踩坑记录\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRTOS内核解析\u003c/strong\u003e：深入剖析实时操作系统的核心机制\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e驱动开发笔记\u003c/strong\u003e：各类外设驱动的设计思路与实现细节\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e工具链使用\u003c/strong\u003e：编译器、调试器、自动化工具的使用技巧\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"为什么写博客\"\u003e为什么写博客？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e知识沉淀\u003c/strong\u003e：写下来能加深理解，也方便日后回顾\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术交流\u003c/strong\u003e：希望能与同行交流，互相学习\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e社区贡献\u003c/strong\u003e：把遇到的问题和解决方案分享出去，帮助有需要的人\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"联系我\"\u003e联系我\u003c/h2\u003e\n\u003cp\u003e如果你有任何问题或建议，欢迎通过以下方式与我交流：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGitHub: \u003ca href=\"https://github.com/freedom413\"\u003e我的GitHub\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e希望我的文章能对你有所帮助！\u003c/p\u003e","title":"欢迎来到我的技术博客"},{"content":"简介 嵌入式软件工程师，专注于底层驱动开发与实时系统。\n技术栈 裸机开发: ARM Cortex-M 系列单片机 RTOS: FreeRTOS、RT-Thread 驱动开发: GPIO、UART、SPI、I2C、ADC等外设 语言: C、嵌入式汇编 博客目的 记录学习过程，分享开发经验，与同行交流技术。\n联系方式 GitHub: 我的GitHub ","permalink":"/about/","summary":"\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e嵌入式软件工程师，专注于底层驱动开发与实时系统。\u003c/p\u003e\n\u003ch2 id=\"技术栈\"\u003e技术栈\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e裸机开发\u003c/strong\u003e: ARM Cortex-M 系列单片机\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRTOS\u003c/strong\u003e: FreeRTOS、RT-Thread\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e驱动开发\u003c/strong\u003e: GPIO、UART、SPI、I2C、ADC等外设\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e语言\u003c/strong\u003e: C、嵌入式汇编\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"博客目的\"\u003e博客目的\u003c/h2\u003e\n\u003cp\u003e记录学习过程，分享开发经验，与同行交流技术。\u003c/p\u003e\n\u003ch2 id=\"联系方式\"\u003e联系方式\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eGitHub: \u003ca href=\"https://github.com/freedom413\"\u003e我的GitHub\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"关于我"}]