
简介这份资源面向使用STM32进行嵌入式开发的工程师与学习者聚焦于在单片机环境中解析和生成JSON格式数据这一常见需求。包内提供基于cjson库的完整工程已验证可直接用于实际项目帮助读者省去移植与调试成本快速掌握轻量级数据交换格式在资源受限平台上的落地方法。压缩包共133个文件约1.76MB以c与h源码文件为主配合Keil工程文件、编译中间产物及readme说明构成可直接打开编译的完整目录结构。目前已有3250人学习下载具备一定的参考基础。读者可从中获得cjson在STM32上的集成范例、工程配置参考与可复用的解析生成代码适合需要处理串口通信、网络传输或配置存储中JSON数据的开发者对照使用。1. 串口收到一包 JSONSTM32 上怎么把它拆干净调试串口的时候最怕对面丢过来一串{temp:25.6,hum:60,dev:node01}人眼看着挺清楚单片机里却是一坨字节流。用strstr找冒号、用atoi硬抠数字字段一多、嵌套一层、数组一来代码立刻变成一锅粥改一个字段要动三处解析逻辑。这份stm32使用cjson解析json字符串.zip解决的就是这个场景把 cJSON 这套轻量 JSON 解析库落到 STM32 上让固件像上位机一样按 key 取值而不是靠字符串截取碰运气。它适合正在做物联网节点、设备上报、上位机协议对接的嵌入式开发者尤其是已经能用串口收发、但协议解析还停留在手写状态机阶段的同学。cjson 与 stm32 的组合不算新但真正把它塞进资源受限的 MCU 并跑稳坑比想象中多。2. 为什么在 MCU 上选 cJSON内存模型与移植边界在 PC 上解析 JSON随手一个库都行到了 STM32RAM 常常只有几十 KB堆还得留给协议栈和缓冲区选型就不能只看「能不能解析」而要看「解析一次要吃掉多少堆、失败时会不会把堆搞碎」。cJSON 之所以在嵌入式圈子里被反复用核心原因是它的数据结构极简一个cJSON结构体同时表达对象、数组、字符串、数字、布尔和 null靠type字段区分链表把兄弟节点串起来父子指针表达嵌套。没有虚函数、没有异常、没有 STL纯 C编译出来体积可控。2.1 cJSON 的对象模型一个结构体打天下理解它的内存模型后面调参和排错才有依据。cJSON 的核心结构大致是这样不同版本字段顺序略有差异以实际源码为准typedef struct cJSON { struct cJSON *next; /* 兄弟节点构成同一层的链表 */ struct cJSON *prev; struct cJSON *child; /* 子节点对象/数组的第一个元素 */ int type; /* 类型标志位如 cJSON_Object、cJSON_String */ char *valuestring; /* 字符串值 */ int valueint; /* 整数值老版本字段 */ double valuedouble; /* 浮点值 */ char *string; /* 当前节点的 key 名 */ } cJSON;一次cJSON_Parse会把整段文本按语法树展开每个{}、每个[]、每个键值对都对应至少一个堆上的cJSON节点字符串值还要额外malloc一份。也就是说解析 200 字节的 JSON堆占用往往在几百字节到 1KB 以上具体取决于字段数量和字符串长度。这就是为什么在 STM32 上必须先算堆、再谈功能。选型上我一般会这样判断如果协议字段固定、层级不超过两层手写解析其实更省一旦字段会增删、要兼容多种上报格式、或者要和上位机共用同一套 JSON 定义cJSON 的通用性就值回票价。它的边界也很清楚——不适合解析超大报文比如几 KB 的配置下发也不适合在中断里调用因为内部全是malloc/free。2.2 移植到 STM32 需要动哪几处cJSON 本身是平台无关的移植工作量主要在「给它一个能用的堆」和「告诉它内存从哪来」。常见做法有两种一是直接用标准库的malloc/free配合启动文件里配置的堆大小二是把 cJSON 的内存钩子重定向到自己管理的静态内存池避免堆碎片。前者上手快后者在长期运行的设备上更稳。先看目录里通常包含的文件cJSON.c、cJSON.h是核心有的包还会带cJSON_Utils.c做 JSON Pointer、合并等高级操作MCU 上一般不用可以不编译。把cJSON.c和cJSON.h加进工程确保编译器能找到头文件路径然后在cJSON.c顶部或工程全局宏里确认没有开启用不到的特性。/* cJSON.c 中与内存相关的钩子默认实现 */ static void *(*cJSON_malloc)(size_t sz) malloc; static void (*cJSON_free)(void *ptr) free; /* 若要在初始化时替换成自己的内存池可在应用层调用 */ void cJSON_InitHooks(cJSON_Hooks *hooks);参数说明cJSON_Hooks里就两个函数指针malloc_fn和free_fn。把它们指向自己实现的my_malloc/my_freecJSON 后续所有节点分配都会走你的池子。这里有个容易忽略的点——cJSON_InitHooks必须在第一次cJSON_Parse之前调用解析中途换钩子会导致 free 时地址对不上直接 HardFault。如果走标准库堆就要回头看一眼启动文件里的Heap_Size。很多模板工程默认只给 0x200512 字节解析一个带三四个字符串字段的 JSON 就可能不够。我一般会把堆调到 0x800 到 0x1000 起步再根据实际报文压测收敛。别嫌浪费堆不够导致的cJSON_Parse返回 NULL 是最难查的那类问题——它不报错只是默默失败。2.3 编译选项与体积控制STM32 上 Flash 也紧张cJSON 编译出来大概几 KB 到十几 KB取决于优化等级和是否带浮点解析。几个能立刻见效的开关把-Os打开如果协议里没有小数确认不需要double解析可以省一些cJSON_Utils.c不参与编译。另外注意cJSON_NESTING_LIMIT这个宏它限制嵌套深度默认 1000对 MCU 来说太大改成 10 左右既能防恶意深嵌套报文把栈打爆又不影响正常协议。/* 在 cJSON.h 或工程宏里收紧缩放 */ #define CJSON_NESTING_LIMIT 10这个值不是随便拍的每层嵌套在递归解析时都占栈STM32 的默认栈常常只有 1KB 左右嵌套 10 层已经接近安全边界。改小之后遇到异常报文会直接解析失败返回 NULL而不是跑飞这比事后查栈溢出划算得多。3. 从零跑通一次解析代码、参数与验证理论清楚了落到能跑的代码。这一章按「准备报文 → 解析 → 取值 → 释放」的完整链路走一遍每一步都给可抄的片段并说明参数怎么改、失败时看哪里。目标读者是已经能在 STM32 上跑串口收发、但还没把 JSON 接进业务逻辑的同学。3.1 最小可运行示例解析一个设备上报包假设串口收到这样一包数据已去掉帧头帧尾只留 JSON 正文{dev:node01,temp:25.6,hum:60,alarm:false}解析代码可以这样写#include cJSON.h #include stdio.h /* 假设 json_buf 是已经收全的 JSON 字符串len 为其长度 */ void parse_report(const char *json_buf) { cJSON *root cJSON_Parse(json_buf); /* 解析整段文本失败返回 NULL */ if (root NULL) { const char *err cJSON_GetErrorPtr(); /* 指向出错位置附近 */ printf(json parse fail near: %s\r\n, err ? err : unknown); return; } cJSON *dev cJSON_GetObjectItemCaseSensitive(root, dev); cJSON *temp cJSON_GetObjectItemCaseSensitive(root, temp); cJSON *hum cJSON_GetObjectItemCaseSensitive(root, hum); cJSON *alarm cJSON_GetObjectItemCaseSensitive(root, alarm); if (cJSON_IsString(dev) dev-valuestring ! NULL) { printf(dev%s\r\n, dev-valuestring); } if (cJSON_IsNumber(temp)) { printf(temp%.1f\r\n, temp-valuedouble); } if (cJSON_IsNumber(hum)) { printf(hum%d\r\n, hum-valueint); } if (cJSON_IsBool(alarm)) { printf(alarm%d\r\n, cJSON_IsTrue(alarm)); } cJSON_Delete(root); /* 必须释放否则每包泄漏一次 */ }逻辑说明cJSON_Parse负责把文本变成树返回根节点cJSON_GetObjectItemCaseSensitive按 key 精确匹配取子节点注意它区分大小写协议里 key 大小写不一致时要用不带CaseSensitive的版本但那个版本匹配更宽松、性能略差我一般协议定死大小写用精确版。取值前一定要用cJSON_IsString/IsNumber/IsBool判类型不能拿到指针就直接读valuestring——字段缺失时返回 NULL字段类型不对时valuestring可能是野指针这是新手最常见的翻车点。参数说明temp用valuedouble读因为 JSON 里是小数hum是整数用valueint读更省事但要注意老版本 cJSON 的valueint有范围截断超过 int 范围的值会失真大数场景统一用valuedouble再强转。alarm是布尔cJSON_IsTrue返回 1/0别直接读valueint判断语义上不干净。3.2 数组与嵌套对象怎么取真实协议很少是平铺的常见的是带数组的传感器列表或者嵌套一层payload。比如{dev:node01,sensors:[{id:1,v:25.6},{id:2,v:60.1}]}取数组要两步先拿到数组节点再用cJSON_GetArraySize和cJSON_GetArrayItem遍历。cJSON *sensors cJSON_GetObjectItemCaseSensitive(root, sensors); if (cJSON_IsArray(sensors)) { int n cJSON_GetArraySize(sensors); /* 元素个数 */ for (int i 0; i n; i) { cJSON *item cJSON_GetArrayItem(sensors, i); cJSON *id cJSON_GetObjectItemCaseSensitive(item, id); cJSON *v cJSON_GetObjectItemCaseSensitive(item, v); if (cJSON_IsNumber(id) cJSON_IsNumber(v)) { printf(sensor[%d] id%d v%.1f\r\n, i, id-valueint, v-valuedouble); } } }这里的关键是cJSON_GetArraySize返回的是当前层元素数遍历时不要边遍历边cJSON_Delete元素否则链表指针会断。要删就整棵树最后统一cJSON_Delete(root)。嵌套对象同理先取外层 key 拿到子对象再在子对象上继续GetObjectItem层级越深越要判 NULL别假设上一层一定存在。3.3 内存释放与解析失败的定位cJSON_Delete(root)是必须的而且只能对cJSON_Parse返回的根节点调用一次。它内部递归释放所有子节点重复调用或对子节点调用都会导致重复 free。我见过有代码在解析失败分支里也去cJSON_Delete(root)但此时root是 NULLcJSON_Delete对 NULL 是安全的这点可以放心真正危险的是把子节点指针存起来、根节点删了之后还去访问那就是 use-after-free。解析失败定位靠cJSON_GetErrorPtr()它返回出错位置附近的指针配合打印偏移量能快速判断是少了引号、多了逗号还是编码问题。注意这个指针指向的是你传入的缓冲区如果缓冲区是复用的接收缓冲打印前别被下一包覆盖。稳妥做法是解析失败时立刻把原始报文和错误偏移一起打到日志里。if (root NULL) { const char *err cJSON_GetErrorPtr(); int offset err ? (int)(err - json_buf) : -1; printf(parse fail at offset %d\r\n, offset); /* 此处可把 json_buf 原样打印便于比对 */ }参数说明offset是相对缓冲区起始的字节偏移不是行号多字节 UTF-8 字符会让偏移看起来偏大这是正常的。定位到偏移后重点看该位置前后几个字节八成是引号、括号或逗号的问题。4. 避坑与排查五个真实翻车现场这一章单独拎出来因为 cJSON 在 STM32 上的问题九成不是库本身的 bug而是内存、生命周期和协议约定没对齐。下面五条都是我在实际项目里踩过或帮人查过的按「现象 → 原因 → 解决」写照着对照能省不少时间。4.1 解析偶发返回 NULL重启又好了现象同一包数据大部分时候解析成功偶尔返回 NULL复位后恢复正常长时间运行后概率变高。原因堆碎片。cJSON 每次解析都malloc一批节点、free一批标准库堆在反复分配释放后会产生碎片某次需要连续较大块时分配失败cJSON_Parse就返回 NULL。报文越长、字段越多越容易触发。解决两条路。短期把启动文件里的Heap_Size调大给碎片留余量长期把 cJSON 的内存钩子重定向到静态内存池池子按最大报文所需节点数预留解析完整体归还从根上消除碎片。我一般选后者尤其是设备要连续跑几个月不重启的场景。4.2 取值时读到乱码或 HardFault现象dev-valuestring打印出来是乱码或者访问某个字段直接进 HardFault。原因没判 NULL 和类型。字段缺失时GetObjectItem返回 NULL直接解引用就崩字段类型和预期不符时比如协议发来数字代码按字符串读valuestring读到的是未初始化或错位的指针。解决每个取值前都走cJSON_IsXxx判断且判断和取值之间不要插入可能修改树的代码。养成「先判类型、再判非空、后取值」的三步习惯这类崩溃能消掉大半。4.3 中文或特殊字符解析失败现象报文里带中文设备名解析在某个偏移处失败英文报文正常。原因编码或转义问题。JSON 规范要求非 ASCII 字符可用\uXXXX转义也可直接 UTF-8但如果发送端把中文按 GBK 发过来cJSON 按 UTF-8 解析就会在字节层面出错。另一种是字符串里出现了未转义的引号或反斜杠。解决先确认收发两端编码一致统一 UTF-8发送端对引号、反斜杠、控制字符做转义。解析失败时用上一章的偏移定位法看失败点是不是正好落在中文字节中间基本就能确认是编码问题。4.4 在中断里解析导致系统卡死现象把cJSON_Parse放进串口中断回调跑一会儿系统无响应。原因cJSON 内部大量malloc/free标准库堆操作不是中断安全的和主循环里的分配竞争会破坏堆结构而且解析耗时不确定长报文会长时间占住中断。解决中断里只做收字节、置标志解析放到主循环或专用任务里做。这是嵌入式的基本纪律但 JSON 场景下特别容易犯因为「收到就解析」写起来最顺手。4.5 重复释放或忘记释放现象运行一段时间后进入 HardFault调用栈指向free或者内存只增不减最终分配失败。原因前者是对同一节点cJSON_Delete了两次或对子节点单独 delete 后又 delete 根后者是解析成功分支忘了 delete每包泄漏一次。解决约定「谁 Parse 谁 Delete只删根节点」子节点一律不单独释放。可以在解析函数出口统一走一个cleanup标签成功失败都到那里释放避免分支漏写。代码评审时重点看cJSON_Parse和cJSON_Delete是否成对出现。5. 进阶把解析封装成可复用模块并做压力验证单次解析跑通只是起点真正上线要解决的是「每包都稳、异常包不崩、内存可观测」。我一般会把 cJSON 包一层对外只暴露业务结构体把 JSON 细节关在模块内部这样协议改字段时业务代码不用跟着动。封装思路是定义业务结构体report_t写一个report_parse(const char *buf, report_t *out)内部完成 Parse、取值、类型校验、Delete返回 0 成功、负值表示具体错误缺字段、类型错、解析失败。这样调用方只关心report_t不碰 cJSON 指针生命周期问题自然消失。typedef struct { char dev[16]; float temp; int hum; int alarm; } report_t; /* 返回值0 成功-1 解析失败-2 字段缺失/类型错 */ int report_parse(const char *buf, report_t *out) { cJSON *root cJSON_Parse(buf); if (!root) return -1; int ret 0; cJSON *dev cJSON_GetObjectItemCaseSensitive(root, dev); cJSON *temp cJSON_GetObjectItemCaseSensitive(root, temp); cJSON *hum cJSON_GetObjectItemCaseSensitive(root, hum); cJSON *alarm cJSON_GetObjectItemCaseSensitive(root, alarm); if (cJSON_IsString(dev) dev-valuestring) { strncpy(out-dev, dev-valuestring, sizeof(out-dev) - 1); out-dev[sizeof(out-dev) - 1] \0; } else { ret -2; } if (cJSON_IsNumber(temp)) out-temp (float)temp-valuedouble; else ret -2; if (cJSON_IsNumber(hum)) out-hum hum-valueint; else ret -2; if (cJSON_IsBool(alarm)) out-alarm cJSON_IsTrue(alarm); else ret -2; cJSON_Delete(root); return ret; }这段封装里有两个细节值得说。一是strncpy后手动补\0因为strncpy在源串长度不小于目标缓冲时不会补终止符直接当字符串用会越界读。二是所有字段校验完再统一返回而不是遇到第一个错就 return这样能一次暴露多个字段问题调试更快。压力验证我一般做三件事。第一用固定报文循环解析一万次观察堆剩余量是否稳定稳定说明没有泄漏第二构造异常报文缺字段、类型错、超长字符串、深嵌套、截断的 JSON逐个喂进去确认都返回错误码而不是崩溃第三把CJSON_NESTING_LIMIT调到很小验证深嵌套报文被安全拒绝。这三步走完基本能放心把模块交给业务层用。还有一个容易被忽略的点接收缓冲和解析缓冲要分开。串口 DMA 收满一包后如果直接在接收缓冲上解析解析期间新数据可能覆盖它导致解析到一半内容变了。稳妥做法是收到完整帧后 memcpy 到独立的解析缓冲再调report_parse。多花一次拷贝换来的是可预期的行为。从那以后我每次把 cJSON 接进新工程都会先做一遍「堆够不够、钩子换没换、异常包喂没喂」这三件事再谈业务逻辑。希望帮到你。本文还有配套的精品资源点击获取