做过嵌入式通信的老哥估计都有这种体会MCU之间、MCU和上位机之间要传数据最早大家要么定一套私有协议要么直接用结构体指针硬转到后来越调越难受。换字段要改协议文档排查数据对不上要先数偏移量上位机那边更是一脸懵。后来开始接触JSON发现在STM32这种资源受限的芯片上做JSON通信只要选对库、写好解析逻辑其实一点都不重。这也是我这次想重点说的——用cJSON在STM32上实现轻量级JSON通信从环境配置、造包发包、解析收包到踩坑复盘完整走一遍。先说结论JSON在嵌入式里不是噱头它是真实能解决开发效率问题的。对接服务器、远程配置设备参数、量产测试工具集数据JSON的灵活性和可读性太有优势了。只要注意内存管理和解析容错完全可以在Cortex-M3、M4这类芯片上跑得稳稳的。这篇文章不讲虚的直接上完整可用的代码和实际项目中踩过的坑看完你基本就能在自己的板子上把JSON通信跑起来。1. 为什么是cJSON嵌入式JSON通信的方案选型与实践思路1.1 JSON通信在MCU场景中的核心价值在说cJSON之前有必要先把一个基本问题聊透MCU上为什么要用JSONJSON是纯文本格式用它传数据会有额外开销而且解析也要消耗CPU周期和传统的二进制协议比起来怎么看都“不划算”。但实际项目里你会发现“不划算”这三个字站不住脚。现在的智能硬件基本都逃不开联网、云平台对接、手机App联动。云平台给设备下发指令格式就是JSON设备上报数据给服务端明文里直接能看到状态智能家居网关、工业数据采集器做协议转换时JSON更是绕不开的中间格式。举一个我自己的例子之前做一款边缘计算网关要同时对接四五个不同厂商的传感器模组每个模组上报的数据结构还不一样如果每接一个设备就写一套二进制解析代码光维护协议版本就够喝一壶的。后来把所有数据统一成JSON格式模组A的数据包和模组B的数据包在网关内部有了统一表达上层再去分发处理代码重构量直接少了一半。这就是JSON的隐性收益生态通用、排障直观、扩展灵活。1.2 cJSON相比“手写拼接”和重型解析库的优势那在STM32上应该选什么库来生成和解析JSON很多新手的第一反应是自己用sprintf拼字符串这我在早期项目里也干过。给一个简单的数据包char buf[128]; sprintf(buf, {\temp\:%.1f,\hum\:%.1f}, 26.5, 60.2);单独看这个语句还好但数据字段一旦多起来、结构一旦嵌套手写拼接就是灾难。你要自己数括号、自己管转义、自己处理字符串长度溢出还要保证每处格式都严格契合。后期需求一变更改一处漏两处的情况太常见了。反过来嵌入式Linux上那些完整度很高的JSON解析库比如json-c、jansson功能确实全但体量重对裸机MCU来说不够轻、依赖也多。cJSON刚好处在中间位置整个库就两个文件cJSON.c和cJSON.h核心代码加起来也就一千多行MIT协议许可可以被无缝集成到任何另一个C项目中。它的数据结构是基于链表的构建和解析JSON树都不需要递归解析整个文档而是按需逐层访问内存占用随着节点数线性增长没有冗余的全局状态。在STM32F103这类只有20KB RAM的芯片上跑只要不是一次处理几KB的超大包基本没有压力。下表是我在实际选型时对比的几个方案方案代码量内存开销易用性适用场景手写sprintf拼接少最小极低只有一两个字段的临时调试cJSON约1.8k行灵活按节点动态分配高大多数STM32/MCU场景json-c较大较大中Linux上位机程序jansson大大中需要复杂文档模型的项目从实际工程角度出发cJSON是性价比最高的选择这也是我把它作为本文主推方案的核心原因。1.3 方案的总体设计思路在正式写代码前需要把整个通信方案的设计思路理清楚。一次完整的JSON通信包括两个方向一是把MCU内部的状态数据序列化成JSON字符串然后通过UART、Wi-Fi模组、以太网等物理通道发出二是从接收到的JSON字符串中把字段提取出来回填到MCU的配置结构体或者触发对应的控制逻辑。在实际项目里我更推荐在STM32的工程目录下单独建一个protocol/或者middleware/文件夹把JSON相关的处理代码统一收进去。这样做的核心考虑是分层解耦底层是串口驱动负责收发字节流中间层是JSON编解码负责字节流和结构化数据之间的转换上层是业务逻辑只和结构体打交道完全不感知JSON的存在。项目后期如果要把UART改成SPI、或者把cJSON换成其他库改动的范围都能被控制在很小的区域内。2. 准备工作把cJSON移植到STM32工程并完成基础配置2.1 源码获取与文件拷贝这步没什么玄学直接从GitHub下载cJSON的源码。打开cJSON.c和cJSON.h这两个文件看整个库的对外依赖非常克制核心就依赖标准库里的stdlib.h、string.h和stdio.h所以把它拉进任何平台都是一件很愉快的事。在Keil MDK工程中我一般的操作方式是在工程目录下新建一个Middlewares/cJSON文件夹。把下载到的cJSON.c和cJSON.h复制进去。在Keil里点击Manage Project Items新建一个分组命名CJSON把cJSON.c加入其中。在Options for Target - C/C的Include Paths里添加Middlewares/cJSON路径。如果用STM32CubeIDE操作更简单直接把文件夹拷到Core同级目录然后右键工程RefreshIDE会自动识别并编译。无论用哪个IDE记住一个原则cJSON.c必须被编译头文件路径必须找得到。这两个点做到位移植就算完成了九成。2.2 C语言标准与内存管理配置cJSON的C代码比较“现代化”它使用了C99的部分特性比如stdint.h中的定长整数类型所以编译标准至少得是C99。Keil MDK默认标准可能是gnu11或c99用AC5编译器的话记得在C/C中把Language C设为C99。虽然cJSON本身没有强制使用//风格注释但C99标准能避免很多莫名其妙的小问题。再就是内存管理。cJSON底层是通过malloc()和free()动态分配内存的STM32裸机上这两个函数的实现直接决定了这个库能不能稳定工作。如果你用的是标准Keil环境需要在启动文件里把堆Heap大小调大我一般分配到4KB以上这个值可以按实际工程调整Heap_Size EQU 0x2000 // 8KB如果工程跑的是FreeRTOS嵌入式开发中我不建议直接频繁调malloc/free——RTOS环境下默认的堆分配器往往不是线程安全的并且频繁动态分配容易产生内存碎片。更规范的做法是给cJSON单独配置内存管理函数用FreeRTOS自带的pvPortMalloc和vPortFree来替换默认实现在工程里需要重定义几个宏。修改方式很简单在编译宏中加入cJSON_mallocpvPortMalloc cJSON_freevPortFree或者在调用cJSON之前通过cJSON_InitHooks函数设置内存管理钩子#include cJSON.h #include FreeRTOS.h void cjson_mem_init(void) { cJSON_Hooks hooks; hooks.malloc_fn pvPortMalloc; hooks.free_fn vPortFree; cJSON_InitHooks(hooks); }这样做的好处有两个一是避免了线程安全问题二是把JSON库的内存消耗纳入了RTOS的堆管理方便统一监控。注意如果配置了cJSON_InitHooks那整个工程所有JSON操作都必须等这个函数执行完再把JSON功能投用建议在系统启动阶段、创建任务之前就把钩子设好。2.3 链接串口底层并确认收发基础cJSON只负责构建和解析JSON字符串真正把字符串发出去、收进来还是要靠STM32的串口。在开始写JSON相关代码之前强烈建议先用最原始的方式确认串口收发是通的比如用一个回环测试程序串口收到一个字符就原样发出去你在PC串口助手里敲一个字符看到回显这就证明底层链路没问题。这一步经验上是必须做的因为如果串口都还没通就去调JSON出了问题根本没法定位。我就遇到过有朋友把JSON报错甩到我面前结果一看他串口的TX和RX压根接反了。基础链路不稳定上层协议再漂亮也是白搭。3. 构建JSONSTM32上生成标准数据包的完整实操3.1 创建对象从零开始“搭”一棵JSON树cJSON的构建方式和搭积木几乎一样。一个JSON对象可以看作一棵树树的根节点是一个JSON对象对应花括号{}或者数组对应方括号[]每个节点就是一个字段。下面我们从最简单的传感数据包开始。比如一个温湿度节点要上报数据数据内容长这样{device_id:node01,type:sensor,temp:26.5,hum:60.2}通过cJSON构建这个JSON字符串的完整代码如下#include cJSON.h #include stdio.h void build_sensor_report(void) { char *json_str NULL; cJSON *root cJSON_CreateObject(); if (root NULL) { return; } cJSON_AddStringToObject(root, device_id, node01); cJSON_AddStringToObject(root, type, sensor); cJSON_AddNumberToObject(root, temp, 26.5); cJSON_AddNumberToObject(root, hum, 60.2); json_str cJSON_PrintUnformatted(root); if (json_str) { // 通过串口发送 json_str uart_send_string((uint8_t *)json_str); free(json_str); } cJSON_Delete(root); }这里有几个关键点需要解释第一cJSON_CreateObject()会创建一个JSON对象节点如果堆内存不足它会返回NULL所以使用前要判断返回指针。第二cJSON_AddXxxToObject系列函数是把子节点挂到指定对象上的便捷API底层会自动创建子节点并链接到树里不需要你手动管理中间节点。第三同时也是最关键的——cJSON_PrintUnformatted(root)会为生成的字符串重新分配一块内存这块内存用完后必须调用free()释放否则每次发一包就泄漏几十到几百字节长时间运行内存肯定被耗尽。我见过太多新手栽在这儿。用cJSON_Print则生成格式化后的带换行和缩进的字符串方便调试但体积更大不太适合无线传输实际生产环境建议用Unformatted版本。3.2 组装嵌套结构对象嵌套与数组的基本用法实际项目里单个平面结构的JSON根本不够用。比如说设备要一次上报多个通道的传感器数据或者要回复一组历史记录这就涉及对象嵌套和数组。cJSON构建嵌套对象的方式同样很顺手下面是含数组的示例{ device_id: node01, channels: [ {ch: 1, value: 23.5}, {ch: 2, value: 68.1} ] }构建代码cJSON *root cJSON_CreateObject(); cJSON *channels cJSON_CreateArray(); cJSON *ch1 cJSON_CreateObject(); cJSON *ch2 cJSON_CreateObject(); cJSON_AddStringToObject(root, device_id, node01); cJSON_AddNumberToObject(ch1, ch, 1); cJSON_AddNumberToObject(ch1, value, 23.5); cJSON_AddItemToArray(channels, ch1); cJSON_AddNumberToObject(ch2, ch, 2); cJSON_AddNumberToObject(ch2, value, 68.1); cJSON_AddItemToArray(channels, ch2); cJSON_AddItemToObject(root, channels, channels); char *out cJSON_PrintUnformatted(root); uart_send_string((uint8_t *)out); free(out); cJSON_Delete(root);注意这一步的写法我先把ch1和ch2通过cJSON_AddItemToArray挂到channels数组上再把整个数组通过cJSON_AddItemToObject挂到根对象上。这里有一个重要的所有权概念当一个子节点被挂到父节点上后这个子节点的内存就归父节点管理了。链表结构里父节点会指向子节点所以后面只需要cJSON_Delete(root)整棵树从根开始递归释放不需要也不能单独去释放ch1、channels这些节点否则就是重复释放。3.3 浮点精度问题与格式化输出嵌入式领域和浮点数打交道的场景特别多温湿度、电压、电流、坐标动不动就是浮点数。cJSON内部用double类型存储数字格式化输出时默认的转换精度是6位小数直接导致一个老生常谈的问题数据的精度被截断。打个比方你测到一个GPS坐标是31.230417用cJSON默认方式打印出来很可能就变成31.230417看着没问题但如果是31.2304168这种精度较高的值默认打印可能就变成31.230417。在传感器校准、金融计费等对小数位数敏感的场景这个误差是致命的。好在cJSON提供了控制小数精度的底层钩子函数cJSON_SetNumberHelper。它的默认实现类似snprintf(buffer, sizeof(buffer), %.6f, num)你可以自己替换成保留更多小数的格式。比如全局保留8位小数#include stdio.h static char *custom_number_helper(char *buffer, const double num) { if (num (double)(int64_t)num) { snprintf(buffer, 16, %d, (int)num); } else { snprintf(buffer, 16, %.8f, num); } return buffer; } void json_precision_config(void) { cJSON_SetNumberHelper(custom_number_helper); }在项目初始化时调用一次json_precision_config()之后所有cJSON构建的数字字段都会保留8位小数。这个技巧在写配套上位机解析时非常实用两边小数点对不上导致的数据异常会少很多。4. 解析JSON把收到的报文安全地转成结构化数据4.1 基本解析流程与类型判断造包相对简单真正考验工程能力的是解析。因为造包是自己往树里填数据数据结构自己心里有数但解析面对的是外部输入你无法保证对方一定会按约定格式发来合法数据。所以解析的核心策略就是四个字层层判断。基础解析流程分三步用cJSON_Parse把字符串解析成树结构。用cJSON_GetObjectItem按字段名取子节点。用cJSON_IsXxx系列函数判断节点类型再取出值。下面是从上一节的温湿度报文里解析出temp值的代码void parse_sensor_report(const char *json_str) { cJSON *root cJSON_Parse(json_str); if (root NULL) { const char *err_pos cJSON_GetErrorPtr(); printf(parse error near: %s\r\n, err_pos ? err_pos : unknown); return; } cJSON *temp_item cJSON_GetObjectItem(root, temp); if (cJSON_IsNumber(temp_item)) { double temp temp_item-valuedouble; printf(temp %.2f\r\n, temp); } else { printf(temp field missing or invalid\r\n); } cJSON *id_item cJSON_GetObjectItem(root, device_id); if (cJSON_IsString(id_item) (id_item-valuestring ! NULL)) { printf(device_id %s\r\n, id_item-valuestring); } cJSON_Delete(root); }这里有一个细节要特别注意cJSON_GetObjectItem找不到字段时会返回NULL找到字段但类型不匹配时访问valuedouble或valuestring成员依然是未定义行为。所以每次取值前务必用cJSON_IsNumber、cJSON_IsString这类类型判断函数把关。不夸张地说嵌入式设备死机、跑飞有相当一部分原因就是解析逻辑里缺少类型检查和空指针判断。4.2 嵌套数据与数组的读取技巧如果说解析平面结构是入门那解析嵌套对象和数组就是日常。假设收到一条指令{cmd: set_channel, params: {ch: 3, value: 88.5}}我们要把它解析成结构体typedef struct { uint8_t channel; float value; } set_channel_t; int parse_set_channel(const char *json_str, set_channel_t *out) { cJSON *root cJSON_Parse(json_str); if (root NULL) return -1; cJSON *params cJSON_GetObjectItem(root, params); if (!cJSON_IsObject(params)) { cJSON_Delete(root); return -1; } cJSON *ch cJSON_GetObjectItem(params, ch); cJSON *val cJSON_GetObjectItem(params, value); if (!cJSON_IsNumber(ch) || !cJSON_IsNumber(val)) { cJSON_Delete(root); return -1; } out-channel (uint8_t)ch-valuedouble; out-value (float)val-valuedouble; cJSON_Delete(root); return 0; }看到没有即使params是嵌套对象只要拿到它的子节点指针后再往下取字段即可逻辑和平面结构是一模一样的cJSON的链表结构天然支持这种逐层向下访问。数组的解析稍微多一步。比如一次要解析一组通道配置{channels: [{ch:1,value:10},{ch:2,value:20}]}遍历数组的固定套路cJSON *array cJSON_GetObjectItem(root, channels); if (cJSON_IsArray(array)) { int len cJSON_GetArraySize(array); for (int i 0; i len; i) { cJSON *item cJSON_GetArrayItem(array, i); cJSON *ch cJSON_GetObjectItem(item, ch); cJSON *value cJSON_GetObjectItem(item, value); if (cJSON_IsNumber(ch) cJSON_IsNumber(value)) { printf(ch%d - %.1f\r\n, (int)ch-valuedouble, value-valuedouble); } } }cJSON_GetArraySize返回数组长度cJSON_GetArrayItem按下标取元素。数组内每个元素又可以是一个完整的JSON对象这正好对应现实中“一组设备状态”“一组历史记录”这类分层数据结构。4.3 解析失败的错误定位与容错机制在工程实践中解析失败几乎是必然会发生的事——对端设备可能中间掉线发来半包可能是不同厂商的设备串口参数没对齐也可能是工具软件自动补了不可见字符。这时候怎么优雅地处理比解析成功更考验代码水平。cJSON本身提供了一个找错入口当cJSON_Parse返回NULL时可以用cJSON_GetErrorPtr()拿到出错位置附近的字符串。我一般在调试阶段把它原样打印出来一眼就能看出是哪里的语法不对。const char *err cJSON_GetErrorPtr(); if (err ! NULL) { printf(JSON error near: %s\r\n, err); }但产品化部署的时候我不会直接依赖这个接口对外报错。更稳妥的方式是定义一套逻辑层的异常码#define JSON_OK 0 #define JSON_ERR_PARSE -1 #define JSON_ERR_FORMAT -2 #define JSON_ERR_MISSING -3在解析函数内部区分错误类型后返回对应错误码业务层只根据错误码做决策比如重发请求、丢弃本包或者进入待机状态。不要试图“修复”不存在的JSON格式MCU端做不了那么聪明的事情。容错的核心思路是解析失败时不崩溃、不卡死记录日志后安全地跳过当前包。这就涉及到另一个重要底线——不要在中断里直接调用cJSON_Parse。串口中断的数据量小还好但如果一次要解析一两百字节甚至更长的JSON包在中断上下文里做动态内存分配和递归遍历尤其是cJSON_Delete释放整棵树时会影响系统的实时性极端情况下还可能引发栈溢出。正确的做法是串口中断只把字节收进环形缓冲区主循环或者独立任务里把完整的一帧数据取出来再交给cJSON解析。中断和业务逻辑分离这条经验适用于所有MCU通信协议。5. 一个完整可落的地的STM32串口JSON通信工程5.1 硬件连接与串口发送框架前面讲了JSON的构建和解析但真正要跑起来串口链路得先搭好。我用的是STM32F103系列做演示用F407、F429也没区别串口1连接USB转TTL模块到PC串口2连接另一个传感器节点或调试串口。PC端用串口助手或者Python脚本发送JSON指令STM32收到后解析、执行并回一条JSON应答。串口发送封装我习惯这样写void uart_send_string(uint8_t *str) { while (*str ! \0) { while (!(USART1-SR USART_FLAG_TXE)); USART1-DR *str; str; } }这只是最基础的轮询发送。数据量大或者需要和RTOS协作时可以改成环形缓冲加DMA发送。但不管怎么改对上层JSON模块来说它只需要一个“能把字符串完整发出去”的接口就够了。5.2 串口接收的定帧处理找包头和结束符接收是更有挑战的一环。因为JSON报文本质上是一串长度不定的字符我们必须自己定义“这一帧从哪里开始、到哪里结束”。业界常见的定帧方式有三种固定长度不适合JSON、包头长度数据适合二进制协议、结束符适合JSON这类可打印文本。我推荐使用\r\n作为JSON报文的结束标志。原因很简单JSON字符串本身就是可打印的ASCII字符\r\n不会出现在结构体内部用它做边界符不会误判。串口中断里每收到一个字节就往缓冲区丢当检测到\n时把缓冲区里的内容作为完整一帧交给JSON解析模块。下面是串口中断接收的简化实现#define RX_BUF_SIZE 512 static uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint16_t rx_index 0; static volatile uint8_t frame_ready 0; void USART1_IRQHandler(void) { if (USART1-SR USART_FLAG_RXNE) { uint8_t ch (uint8_t)(USART1-DR 0xFF); if (ch \n) { rx_buf[rx_index] \0; frame_ready 1; rx_index 0; } else { if (rx_index RX_BUF_SIZE - 1) { rx_buf[rx_index] ch; } else { rx_index 0; // 溢出丢弃当前帧重新开始 } } } }主循环里检测到frame_ready 1就把rx_buf丢给JSON解析。这里有个关键点缓冲区大小必须预留足量并且对超出缓冲区的情况要做丢弃处理——宁愿丢包也不要让一个超长帧把缓存撑爆。5.3 完整示例接收控制指令并回发状态应答现在把收发串起来写一个完整Demo。功能设计上位机下发{cmd:query}节点返回当前温湿度。上位机下发{cmd:set_threshold,value:30.5}节点设置阈值并返回设置结果。MCU端完整代码结构void process_json_command(const char *json_str) { cJSON *root cJSON_Parse(json_str); if (root NULL) { cJSON *resp cJSON_CreateObject(); cJSON_AddStringToObject(resp, status, error); cJSON_AddStringToObject(resp, msg, parse_error); char *out cJSON_PrintUnformatted(resp); uart_send_string((uint8_t *)out); free(out); cJSON_Delete(resp); return; } cJSON *cmd cJSON_GetObjectItem(root, cmd); if (cJSON_IsString(cmd)) { if (strcmp(cmd-valuestring, query) 0) { cJSON *resp cJSON_CreateObject(); cJSON_AddStringToObject(resp, status, ok); cJSON_AddNumberToObject(resp, temp, read_temp()); cJSON_AddNumberToObject(resp, hum, read_hum()); char *out cJSON_PrintUnformatted(resp); uart_send_string((uint8_t *)out); free(out); cJSON_Delete(resp); } else if (strcmp(cmd-valuestring, set_threshold) 0) { cJSON *val cJSON_GetObjectItem(root, value); if (cJSON_IsNumber(val)) { set_threshold((float)val-valuedouble); cJSON *resp cJSON_CreateObject(); cJSON_AddStringToObject(resp, status, ok); cJSON_AddNumberToObject(resp, threshold, val-valuedouble); char *out cJSON_PrintUnformatted(resp); uart_send_string((uint8_t *)out); free(out); cJSON_Delete(resp); } } else { cJSON *resp cJSON_CreateObject(); cJSON_AddStringToObject(resp, status, unknown_cmd); char *out cJSON_PrintUnformatted(resp); uart_send_string((uint8_t *)out); free(out); cJSON_Delete(resp); } } cJSON_Delete(root); }主循环while (1) { if (frame_ready) { frame_ready 0; process_json_command((char *)rx_buf); } // 其他业务任务... }这个工程麻雀虽小五脏俱全。它包含了JSON解析、类型检查、异常应答、命令分发、JSON构造和串口收发基本覆盖了MCU上JSON通信的所有核心环节。6. 实测中的常见问题与排查技巧实录6.1 常见报错和异常现象的快速定位做嵌入式开发调试过程永远是“惊喜”不断JSON通信尤其如此。我把实际项目中踩过的高频问题整理成一张速查表遇到问题时可以先按图索骥现象可能原因处理办法cJSON_Parse返回NULL堆内存不足、报文被截断、格式非法增大堆空间打印错误指针附近内容定位解析出的数值都是0字段名拼写错误或类型不匹配检查字段名大小写用cJSON_IsNumber确认类型字符串解析出来是乱码串口波特率不匹配或数据位数配置错误核对两边串口参数用串口助手看原始字节程序运行几天后死机每次发JSON后忘记free()内存泄漏检查cJSON_PrintUnformatted和cJSON_Parse的返回值释放情况收到的JSON总是半包缓冲区不够大或定帧逻辑有缺陷调大RX_BUF_SIZE检查结束符\n是否被意外吞掉浮点数据对上不齐小数精度默认6位精度不足用cJSON_SetNumberHelper自定义输出精度6.2 内存泄漏嵌入式JSON开发的头号杀手如果说只能选一个最值得重点关注的坑那一定是内存泄漏。cJSON_CreateObject分配内存cJSON_PrintUnformatted分配内存cJSON_Parse分配内存——这三条龙但凡有一条的返回值没有正确释放长时间运行后设备就会出现各种诡异问题先是大包解析失败然后是程序卡死最后整个系统重启。这里有一个实用的自检习惯每次调试完记得把功能跑长时间稳定性测试或者在对内存敏感的版本中用如下方式做压力自检——连续构建、打印、删树一千万次观察系统内存趋势是否稳定。下面是一个简单的压力测试思路for (uint32_t i 0; i 1000000; i) { cJSON *root cJSON_CreateObject(); cJSON_AddStringToObject(root, id, stress); cJSON_AddNumberToObject(root, cnt, i); char *out cJSON_PrintUnformatted(root); free(out); cJSON_Delete(root); }如果跑完这个循环后系统剩余RAM和开始时差不多说明内存管理基本健康。如果发现可用内存不断减少回头检查每一处cJSON_Print和cJSON_Parse的释放逻辑。6.3 嵌入式JSON的几个工程经验最后分享几条我踩过多次后总结出的经验纯属个人体会第一凡是JSON字符串的缓冲区都尽量定义成静态数组不要用栈上的小数组。特别是在RTOS环境任务栈大小有限一个几百字节的JSON字符串加上临时变量很容易把栈撑爆。如果不确定报文最大长度直接把缓冲区的长度定义成该场景物理上限比如串口一帧最多1024字节那rx_buf就定义1024 8。第二不要把cJSON_Parse放进中断或者定时器回调。之前提到过一次但值得反复强调中断里只做数据搬运不做格式解析。有一次我把解析放在了定时器中断里测表面上功能正常但系统实时性明显恶化最后排查下来就是中断里做了大量的malloc/free阻塞了其他高优先级任务。第三所有JSON字段名尽量集中管理。要么定义成#define宏要么放到一个统一的头文件里让上位机、下位机共用同一份字段定义。我吃过亏的典型场景是MCU端写的字段是deviceId服务端开发在文档里写成了device_id两边联调时白白浪费了两天时间。字段名统一用下划线风格或者驼峰风格并不重要重要的是所有人都遵守同一套规范。cJSON在STM32上的实战基本就是这么一回事掌握好创建、解析、删除这三个闭环再重视内存管理和边界判断剩下的就是不断积累现场经验。如果是在自己的项目里从头开始建议先把我上面的Demo跑通再逐步替换成自己的数据结构和业务逻辑。调试永远是嵌入式开发的必修课只要第一次把完整的JSON链路跑通后续再碰到类似需求就是体力活了。