
简介cJson库是专为嵌入式系统打造的轻量级JSON解析与生成工具面向STM32等单片机开发者解决资源受限环境下JSON数据交互的难题。压缩包内共2个文件包含1个cJSON.h头文件与1个cJSON.c实现文件整体仅9KB集成时只需将这两个文件加入工程即可无需额外依赖。目前已有790人学习下载适合需要处理网络协议、配置更新等JSON通信场景的嵌入式工程师。资源围绕cJson核心能力展开涵盖JSON基础概念、库的轻量高效与内存管理特点、cJSON_Parse、cJSON_Print、cJSON_CreateObject、cJSON_Delete等核心函数用法并给出从导入库、创建对象、添加键值对到解析、遍历、生成字符串及释放资源的完整流程同时结合STM32平台说明实际应用方式附有可直接参考的示例代码帮助读者快速掌握在单片机项目中读写JSON数据的思路与方法。1. 串口屏配置数据总丢我把 cJSON 塞进 STM32 的完整记录前阵子调一块 STM32 板子上位机通过串口下发一组 JSON 格式的配置参数MCU 这边解析完再回传状态。一开始我图省事拿字符串一顿strstr加sscanf硬抠字段结果字段顺序一变、嵌套一层对象、或者数值带个负号解析就翻车。后来换成 cJSON整个流程才稳下来。cJSON 是一个用纯 C 写的超轻量 JSON 解析与生成库核心就一个.c加一个.h不依赖任何第三方堆内存占用可控天生适合 STM32、GD32 这类资源紧张的单片机场景。它解决的就是「在裸机或 RTOS 上可靠地读写 JSON」这件事。如果你正在做串口屏、物联网模组、上位机通信协议或者单纯想让 MCU 的配置管理不那么玄学这份资源值得你花半小时跟一遍。2. 为什么单片机项目里我最终选了 cJSON选型对比与内存模型2.1 裸机解析 JSON 的三种常见做法在 MCU 上处理 JSON我见过也试过三种路子。第一种是纯字符串匹配strstr找 key再往后抠 value。优点是零依赖、零额外内存缺点是遇到嵌套、转义、数组就基本歇菜维护成本极高。第二种是手写状态机逐字符扫描性能最好但开发周期长改一次协议就要重写一遍属于「写的时候爽改的时候想哭」。第三种就是引入现成的解析库cJSON 是这里面最省心的一个。cJSON 的定位很明确不追求极致性能追求「够小、够简单、够可靠」。整个库编译出来在 ARM Cortex-M 上大概几 KB 的 FlashRAM 开销取决于你解析的 JSON 大小因为它用的是动态内存分配。它支持标准的 JSON 语法包括对象、数组、字符串、数字、布尔和 null解析和生成两个方向都覆盖。对于绝大多数单片机通信场景这个能力边界完全够用。2.2 cJSON 的内存模型为什么它能在单片机上跑cJSON 的核心数据结构是一个双向链表节点cJSON每个节点代表 JSON 里的一个值。解析时它用malloc为每个节点分配内存字符串值再单独分配一块。这意味着两件事第一你的堆空间必须够第二用完必须调cJSON_Delete释放否则就是内存泄漏。很多人担心malloc在单片机上不安全。实际项目中只要你的堆不是小得可怜一般建议至少留 2KB 以上给 JSON 解析并且解析的 JSON 不会无限大cJSON 是稳的。我一般会在FreeRTOS里给它单独划一块 heap或者用heap_4这种带碎片合并的方案。裸机的话确保_Min_Heap_Size在启动文件里设够。提示cJSON 默认用malloc/free如果你有自己的内存池可以在cJSON.c开头重定义cJSON_malloc和cJSON_free宏指向你的池分配函数。2.3 移植到 STM32 工程文件添加与堆配置移植本身没有难度把cJSON.c和cJSON.h拷进工程加入编译然后确认堆大小。以 Keil MDK 为例在启动文件startup_stm32xxxx.s里找到Heap_Size默认可能是0x200512 字节这个值解析稍大一点的 JSON 就会失败。我一般改成0x800到0x1000。/* startup_stm32xxxx.s 片段调整堆大小 */ Heap_Size EQU 0x00001000 ; 4KB 堆给 cJSON 留足空间改完堆之后在需要用的地方包含头文件即可#include cJSON.h逻辑说明Heap_Size决定了malloc能用的最大连续内存。cJSON 解析一个包含 10 个字段的对象大约需要几百字节到 1KB 出头具体取决于字符串长度。参数上0x1000是 4KB对大多数配置类 JSON 足够如果你要解析上百个字段的报文继续往上加。3. 解析与生成实战从串口收到一帧 JSON 到回传状态3.1 解析把串口缓冲区变成 cJSON 对象假设串口收到这样一帧数据存在rx_buf里{dev:sensor01,temp:25.6,hum:60,alarm:false,mode:[1,2,3]}解析代码#include cJSON.h #include string.h void parse_rx_frame(char *rx_buf) { cJSON *root cJSON_Parse(rx_buf); /* 解析整帧 */ if (root NULL) { const char *err cJSON_GetErrorPtr(); /* 拿到出错位置 */ printf(json parse error near: %s\r\n, err); return; } /* 取字符串字段 */ cJSON *dev cJSON_GetObjectItem(root, dev); if (cJSON_IsString(dev)) { printf(dev %s\r\n, dev-valuestring); } /* 取数值字段 */ cJSON *temp cJSON_GetObjectItem(root, temp); if (cJSON_IsNumber(temp)) { printf(temp %.1f\r\n, temp-valuedouble); } /* 取布尔字段 */ cJSON *alarm cJSON_GetObjectItem(root, alarm); if (cJSON_IsBool(alarm)) { printf(alarm %d\r\n, cJSON_IsTrue(alarm)); } /* 取数组字段 */ cJSON *mode cJSON_GetObjectItem(root, mode); if (cJSON_IsArray(mode)) { int n cJSON_GetArraySize(mode); for (int i 0; i n; i) { cJSON *item cJSON_GetArrayItem(mode, i); printf(mode[%d] %d\r\n, i, item-valueint); } } cJSON_Delete(root); /* 必须释放否则内存泄漏 */ }逻辑说明cJSON_Parse会扫描整个字符串并构建链表树返回根节点。失败时返回NULL用cJSON_GetErrorPtr能定位到出错的大致位置调试时很有用。取值时不要直接访问valuestring或valueint先用cJSON_IsString、cJSON_IsNumber这类宏判断类型否则类型不匹配会读到脏数据。数组用cJSON_GetArraySize配合cJSON_GetArrayItem遍历。最后cJSON_Delete是必须的它递归释放整棵树。参数说明cJSON_GetObjectItem对大小写敏感key 必须完全匹配。valueint是整数截断值valuedouble是浮点原值取浮点一定用valuedouble。3.2 生成把 MCU 状态打包成 JSON 回传回传状态时用 cJSON 的构建接口从零拼一个对象char *build_status_json(void) { cJSON *root cJSON_CreateObject(); /* 创建根对象 */ cJSON_AddStringToObject(root, dev, sensor01); cJSON_AddNumberToObject(root, temp, 25.6); cJSON_AddBoolToObject(root, alarm, 0); cJSON *mode cJSON_AddArrayToObject(root, mode); /* 创建数组 */ cJSON_AddItemToArray(mode, cJSON_CreateNumber(1)); cJSON_AddItemToArray(mode, cJSON_CreateNumber(2)); cJSON_AddItemToArray(mode, cJSON_CreateNumber(3)); char *out cJSON_PrintUnformatted(root); /* 无空格紧凑输出 */ cJSON_Delete(root); /* 释放树out 独立存在 */ return out; /* 用完记得 free(out) */ }逻辑说明cJSON_CreateObject创建空对象cJSON_AddStringToObject等接口直接往对象里挂键值对。数组用cJSON_AddArrayToObject创建后再用cJSON_AddItemToArray逐个添加元素。输出用cJSON_PrintUnformatted得到不带换行和空格的紧凑字符串适合串口传输cJSON_Print会带缩进适合调试打印。参数说明cJSON_PrintUnformatted返回的字符串是malloc出来的发完串口后必须free(out)否则泄漏。cJSON_Delete(root)只释放树本身不影响已经打印出来的字符串。3.3 在 FreeRTOS 任务里安全使用 cJSON如果跑 RTOS多个任务同时调 cJSON 的malloc/free会有线程安全问题。常见做法是给 cJSON 加一把互斥锁或者干脆把 JSON 解析集中到一个任务里其他任务通过队列把缓冲区传进来。/* 在 cJSON.c 中重定义分配函数指向线程安全的堆 */ #define cJSON_malloc(sz) pvPortMalloc(sz) #define cJSON_free(p) vPortFree(p)逻辑说明FreeRTOS 的pvPortMalloc在heap_4及以上是线程安全的内部有挂起调度器保护。把 cJSON 的分配器替换掉就不用额外加锁。注意这要求你在包含cJSON.h之前先包含FreeRTOS.h。参数说明heap_4带碎片合并适合频繁分配释放的场景heap_1不支持 free不能用。堆总大小在configTOTAL_HEAP_SIZE里配置给 cJSON 留够余量。4. 避坑与排查cJSON 在单片机上最容易翻车的五个点4.1 解析返回 NULL但串口数据看着没问题现象cJSON_Parse返回NULL用cJSON_GetErrorPtr打印出错位置发现停在一个看似正常的字符上。原因最常见的是串口接收缓冲区没有加字符串结束符\0。cJSON 靠\0判断字符串结尾如果缓冲区里是裸数据它会一直往后读读到非法字符就报错。解决收到数据后先手动补\0再传给cJSON_Parse。另外确认缓冲区长度够别让 JSON 被截断。rx_buf[len] \0; /* 补结束符 */ cJSON *root cJSON_Parse(rx_buf);4.2 解析几次之后程序跑飞或死机现象单次解析正常循环解析几十次后malloc返回NULL或者直接进 HardFault。原因忘记调cJSON_Delete每次解析都泄漏一棵树堆很快耗尽。解决cJSON_Parse和cJSON_Delete必须成对出现。用cJSON_Print系列生成的字符串也要free。我一般会在解析函数出口统一释放避免中途return漏掉。4.3 浮点数取出来是 0 或者整数现象JSON 里写的是25.6用valueint取出来是 25用valuedouble取出来是 0。原因valueint是整数截断浮点必须用valuedouble。如果valuedouble也是 0检查是不是在cJSON.h里把CJSON_DISABLE_FLOAT打开了或者编译器没开浮点支持。解决取浮点统一用valuedouble并确认没有定义禁用浮点的宏。4.4 中文或特殊字符解析后乱码现象JSON 里的中文字段解析出来是乱码。原因cJSON 默认按 UTF-8 处理字符串如果你的串口数据是 GBK 编码解析出来自然不对。解决统一用 UTF-8 编码传输。如果上位机发的是 GBK要么在上位机转码要么在 MCU 侧做一次编码转换再解析。4.5 堆够大但解析大 JSON 仍然失败现象Heap_Size已经调到 8KB解析一个稍大的 JSON 还是返回NULL。原因堆是够的但内存碎片导致没有连续大块可用。频繁解析不同大小的 JSON 会加剧碎片。解决换用带碎片合并的堆管理如 FreeRTOSheap_4或者给 cJSON 单独划一块静态内存池重定义cJSON_malloc/free指向池分配。另一个思路是限制单帧 JSON 的最大长度从源头控制内存需求。5. 进阶技巧用 cJSON 做配置持久化与版本兼容5.1 把配置存进 Flash 再读回来cJSON 不只能解析串口数据拿来做配置持久化也很顺手。思路是上电时从 Flash 读出一段 JSON 字符串解析成对象把参数加载到全局变量参数修改后重新生成 JSON 字符串擦写 Flash 存回去。/* 从 Flash 读取配置并解析 */ void load_config_from_flash(void) { char buf[512]; flash_read(CFG_ADDR, (uint8_t *)buf, sizeof(buf)); buf[sizeof(buf) - 1] \0; cJSON *root cJSON_Parse(buf); if (root NULL) { /* 首次上电或数据损坏加载默认配置 */ load_default_config(); return; } cJSON *baud cJSON_GetObjectItem(root, baud); if (cJSON_IsNumber(baud)) { g_config.baud baud-valueint; } /* ... 其他字段 ... */ cJSON_Delete(root); }逻辑说明Flash 里存的是上一次cJSON_PrintUnformatted生成的紧凑字符串。读取后补\0再解析。解析失败说明 Flash 数据无效首次上电或擦写中断走默认配置分支。参数上buf大小要大于你最大配置 JSON 的长度留一点余量。5.2 用版本字段做配置兼容产品迭代时配置结构会变老版本 Flash 里的 JSON 可能缺字段或多字段。我的习惯是在 JSON 根对象里放一个ver字段加载时先读版本号再决定按哪套逻辑解析。cJSON *ver cJSON_GetObjectItem(root, ver); int cfg_ver cJSON_IsNumber(ver) ? ver-valueint : 0; if (cfg_ver 2) { /* 老版本配置做字段迁移 */ migrate_config_v1_to_v2(root); }逻辑说明ver缺失时默认按版本 0 处理保证向前兼容。迁移函数负责把老字段映射到新结构或者补上新增字段的默认值。这样升级固件后老设备不会因为配置格式变化而丢参数。参数说明版本号建议用整数每次结构变更递增。迁移逻辑要幂等重复执行不出错。5.3 一个我常用的调试习惯cJSON 出错时cJSON_GetErrorPtr返回的是出错位置附近的指针但它指向的是原始缓冲区内部。我一般会打印出错位置前后各 16 个字符这样能直观看到是哪个字段附近出的问题。const char *err cJSON_GetErrorPtr(); if (err) { int offset err - rx_buf; printf(error at offset %d: %.32s\r\n, offset, rx_buf (offset 16 ? offset - 16 : 0)); }从那以后我每次集成 cJSON 到新工程都强制先跑一遍「解析失败路径」——故意发一帧残缺 JSON确认错误定位和内存释放都正常再跑正常流程。这个习惯帮我省了不少现场排查的时间。希望帮到你。本文还有配套的精品资源点击获取