ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

嵌入式开发中JSON的应用:从核心价值到cJSON实战解析

嵌入式开发中JSON的应用:从核心价值到cJSON实战解析 1. 从零开始嵌入式开发者为何绕不开JSON如果你是一名嵌入式开发者最近在捣鼓一个智能家居的传感器节点或者一个工业数据采集器当你需要把设备采集到的温度、湿度、状态码发送到云端服务器时你第一个想到的数据格式是什么五年前答案可能是自定义的二进制协议或者简单的逗号分隔文本。但今天越来越多的开发者会不假思索地回答JSON。这听起来有点反直觉。嵌入式系统尤其是资源受限的MCU微处理器内存以KB计主频几十MHz连跑个完整的操作系统都费劲为什么要用JSON这种“臃肿”的、基于文本的、带一堆大括号和引号的数据格式我最初也有这个疑问直到在一个实际项目中被迫用JSON与一个现成的云平台对接。那次经历彻底改变了我的看法在物联网和互联设备成为标配的时代JSON不再是“可选”而是“必选”。它就像设备与外部世界手机App、云端服务器、Web前端沟通的“普通话”虽然对设备本身来说有点“口音”但为了能跟别人顺畅聊天你必须得学。简单来说JSON是一种轻量级的数据交换格式它采用完全独立于语言的文本格式但使用了类似于C语言家族包括C C C# Java JavaScript Python等的习惯。这种特性让它成了连接嵌入式C语言世界和外部丰富生态的完美桥梁。你不需要在设备端实现复杂的XML解析器也不需要为每个新对接的服务器定制二进制协议。用JSON你的设备数据可以几乎无缝地被任何现代编程语言和平台理解。对于嵌入式开发者而言学习JSON的核心价值在于提升开发效率、降低系统耦合度、以及利用庞大的现有工具链。这篇文章我就从一个老嵌入式工程师的角度带你从零开始把JSON在嵌入式领域的里里外外摸个透。2. JSON在嵌入式系统的核心价值与选型考量2.1 为什么是JSON不仅仅是“流行”很多嵌入式同行对JSON的第一反应是抗拒认为它“浪费资源”。确实相比精心设计的二进制协议JSON在数据体积和解析开销上都不占优。但我们需要跳出单一的“资源消耗”视角用系统工程的角度来权衡。首要价值是互操作性。这是JSON的杀手锏。你开发一个智能插座手机端同事用Swift写iOS App前端同事用JavaScript写管理后台后端同事用Go写服务器。如果你用自定义二进制协议你需要为每个端编写详细的协议文档并分别实现编解码库任何一方的改动都可能引发连锁反应调试更是噩梦。而使用JSON你只需要在设备端生成一个像{power: true, current: 125, timestamp: 1678886400}这样的字符串其他所有平台都可以用标准库直接解析毫无障碍。这种跨平台、跨语言的“通用语”能力极大地降低了协作成本。其次是可读性与调试便利性。通过串口打印出一行0x01 0x00 0x7D 0x64 0x00 0x1A你需要对照协议手册才能知道这是什么意思。但如果打印出{power: false, current: 125, voltage: 220}任何人都能一眼看懂。在开发调试阶段这种可读性可以节省大量时间。你可以直接用电脑上的串口工具、网络调试助手查看数据甚至用printf打印出来肉眼检查无需额外的解码工具。最后是生态与工具链。互联网世界围绕JSON建立了庞大的工具生态。从数据验证工具如JSON Schema到格式美化工具再到各种在线测试和模拟器。你的设备API可以很容易地用Postman等工具进行测试云端数据库如MongoDB可以直接存储JSON文档。这意味着你可以站在巨人的肩膀上快速构建系统而不是一切从零开始。注意选择JSON并不意味着完全抛弃二进制协议。在设备内部模块间通信或对带宽、实时性要求极高的传感器原始数据流传输场景二进制协议仍是首选。JSON更适合用于设备与外部系统的配置下发、状态上报、命令与控制等交互频率相对较低、但对灵活性和互操作性要求高的场景。2.2 资源评估你的MCU“吃得消”吗这是最实际的问题。引入JSON库主要带来两方面的开销ROM程序存储空间和RAM运行内存。ROM开销一个典型的轻量级JSON解析库如cJSON编译后的代码体积大约在20KB - 40KB左右这取决于编译器的优化等级和启用的功能。对于拥有128KB以上Flash的现代主流MCU如STM32F1/F4系列、ESP32、nRF52系列这个开销通常是可接受的。如果MCU只有32KB或更少的Flash你就需要慎重考虑或者寻找更极致的轻量级实现。RAM开销这是更大的挑战。JSON解析需要在内存中构建一棵树tree来表示数据结构这会在解析过程中动态分配内存。一个包含几十个键值对的复杂JSON对象其内存占用可能轻松达到几百字节甚至几KB。对于仅有几KB RAM的MCU如某些8位MCU或低端ARM Cortex-M0这可能是无法承受的。应对策略使用流式解析器SAX模型不同于一次性将整个JSON文档加载到内存树中DOM模型流式解析器在读取JSON文本时遇到一个元素如对象开始、键、值、对象结束就触发一个回调函数。这样你只需要在回调函数中处理当前的数据无需在内存中保存整个文档结构。这极大地降低了峰值RAM使用量特别适合处理大型或深度嵌套的JSON。开源库如jsmn就是基于此模型。使用内存池或静态缓冲区替代系统默认的malloc/free使用预先分配好的固定大小内存池来分配JSON对象可以避免内存碎片并精确控制内存上限。精简JSON数据在设计协议时尽量使用简短的键名如用t代替temperature减少不必要的嵌套使用数字代替布尔值和字符串枚举。2.3 开源库选型几个主流选择深度对比在嵌入式C语言环境中你有几个成熟的开源JSON库可以选择。下表是我根据多个项目经验整理的对比库名称核心特点内存模型易用性适合场景大致ROM占用cJSON经典、功能全面、文档丰富。采用DOM模型在内存中构建完整的对象树。动态分配malloc/free极高。API直观创建、解析、修改、序列化都很方便。资源相对充裕RAM 10KB需要频繁修改或随机访问JSON数据的场景。25-40 KBjsmn极致轻量、单文件、零动态内存分配。采用流式SAX模型。静态/栈内存。解析时只需一个令牌token数组。中。需要自己遍历token数组来提取数据代码稍多。资源极其紧张RAM 4KB只需解析不需生成JSON且数据结构固定的场景。1-3 KBJansson功能强大、类型安全、错误处理完善。类似cJSON的DOM模型。动态分配高。API设计严谨有更严格的类型检查。对代码健壮性要求高项目相对复杂资源足够的场景。30-50 KBparson轻量、单文件、DOM模型。API设计模仿cJSON但更简洁。动态分配高。比cJSON更简洁内存管理选项更多。需要DOM模型的便利性但又觉得cJSON稍显臃肿的折中选择。15-25 KB我的个人建议对于大多数从零开始的嵌入式开发者如果你的MCU资源不是捉襟见肘比如有几十KB RAM我强烈推荐从cJSON开始。它的高易用性能让你快速上手理解JSON在C语言中的操作范式把精力集中在业务逻辑上而不是与解析库搏斗。当你遇到性能或内存瓶颈时再考虑换用jsmn这类更轻量的库。cJSON的源码结构清晰也是学习如何用C实现动态数据结构的优秀范例。3. 实战入门使用cJSON解析与生成数据理论说了这么多是时候动手了。我们以最流行的cJSON为例看看在嵌入式C项目中如何具体地解析一段来自云端的配置又如何生成一段要上报的状态数据。3.1 项目集成与基础API首先你需要获取cJSON的源码。通常就是一个cJSON.c和一个cJSON.h文件。将它们添加到你的工程中。确保你的编译器支持标准C库因为cJSON使用了stdio.h,stdlib.h,string.h等。如果你的平台没有标准的malloc/free你需要修改cJSON.c开头的malloc,free等宏定义指向你的内存管理函数。接下来认识几个最核心的API它们是你操作JSON的“三板斧”cJSON_Parse(const char *value): 这是入口函数。给你一个JSON格式的字符串例如从串口接收到的它返回一个指向cJSON结构体的指针。这个指针就是整个JSON文档的根。如果返回NULL说明解析失败可能是字符串格式不对或者内存不足。你可以用cJSON_GetErrorPtr()获取错误位置。cJSON_GetObjectItem(const cJSON *object, const char *string): 从JSON对象中根据键名获取对应的值项。这是你最常用的函数。例如cJSON_GetObjectItem(root, temperature)会返回一个代表温度值的cJSON项。cJSON_Print(const cJSON *item): 将一个cJSON结构体通常是根序列化格式化成一个漂亮的、带缩进的JSON字符串。这个字符串是动态分配内存的用完后需要free()。cJSON_PrintUnformatted(const cJSON *item): 同上但生成紧凑的、没有空白字符的JSON字符串更适合网络传输。cJSON_Delete(cJSON *item): 删除一个cJSON项及其所有子项并释放内存。非常重要对于任何通过cJSON_Parse或创建函数得到的指针最后都必须调用此函数来避免内存泄漏。3.2 场景一解析云端下发的配置假设你的智能灯接收到了来自手机App的这样一条JSON配置信息{ device_id: light_001, brightness: 80, color: { r: 255, g: 100, b: 50 }, auto_off: true, schedule: [18, 30, 22, 0] }你的设备需要解析它并设置相应的灯光参数。#include cJSON.h void parse_device_config(const char *json_str) { // 1. 解析JSON字符串 cJSON *root cJSON_Parse(json_str); if (root NULL) { const char *error_ptr cJSON_GetErrorPtr(); if (error_ptr ! NULL) { printf(Error before: %s\n, error_ptr); } // 这里应该进行错误处理比如回复一个错误码给云端 return; } // 2. 提取设备ID (字符串) cJSON *device_id_item cJSON_GetObjectItem(root, device_id); if (cJSON_IsString(device_id_item) (device_id_item-valuestring ! NULL)) { printf(Config for device: %s\n, device_id_item-valuestring); // 你可以将设备ID保存到某个变量或结构体中 } // 3. 提取亮度 (数字) cJSON *brightness_item cJSON_GetObjectItem(root, brightness); if (cJSON_IsNumber(brightness_item)) { int brightness brightness_item-valueint; // 获取整数值 // brightness_item-valuedouble 获取浮点值 printf(Set brightness to: %d\n, brightness); // 调用你的PWM设置函数例如 set_led_brightness(brightness); } // 4. 提取嵌套的颜色对象 cJSON *color_item cJSON_GetObjectItem(root, color); if (cJSON_IsObject(color_item)) { cJSON *r_item cJSON_GetObjectItem(color_item, r); cJSON *g_item cJSON_GetObjectItem(color_item, g); cJSON *b_item cJSON_GetObjectItem(color_item, b); if (cJSON_IsNumber(r_item) cJSON_IsNumber(g_item) cJSON_IsNumber(b_item)) { uint8_t r (uint8_t)r_item-valueint; uint8_t g (uint8_t)g_item-valueint; uint8_t b (uint8_t)b_item-valueint; printf(Set color to RGB(%d, %d, %d)\n, r, g, b); // 设置RGB LED颜色 } } // 5. 提取自动开关标志 (布尔值) cJSON *auto_off_item cJSON_GetObjectItem(root, auto_off); if (cJSON_IsBool(auto_off_item)) { bool auto_off_enabled cJSON_IsTrue(auto_off_item); // 返回1表示true printf(Auto-off is %s\n, auto_off_enabled ? enabled : disabled); } // 6. 提取定时数组 cJSON *schedule_item cJSON_GetObjectItem(root, schedule); if (cJSON_IsArray(schedule_item)) { int array_size cJSON_GetArraySize(schedule_item); printf(Schedule has %d entries: , array_size); // 遍历数组 cJSON *element NULL; cJSON_ArrayForEach(element, schedule_item) { if (cJSON_IsNumber(element)) { printf(%d , element-valueint); } } printf(\n); // 假设数组固定为4个元素 [on_hour, on_minute, off_hour, off_minute] if (array_size 4) { cJSON *on_hour cJSON_GetArrayItem(schedule_item, 0); cJSON *on_minute cJSON_GetArrayItem(schedule_item, 1); // ... 获取并设置定时器 } } // 7. 最后务必释放内存 cJSON_Delete(root); }关键点解析类型检查至关重要在访问valuestring或valueint之前一定要用cJSON_IsString,cJSON_IsNumber,cJSON_IsBool等函数检查类型。如果云端下发的brightness意外地是字符串80而你直接当数字用程序可能会崩溃或行为异常。嵌套访问对于嵌套的对象像访问color.r一样先取得color对象再从这个对象里取r。数组遍历cJSON_ArrayForEach宏提供了遍历数组的简洁方法。你也可以用cJSON_GetArrayItem(array, index)按索引访问。3.3 场景二生成设备状态上报数据现在你的设备需要周期性地向服务器报告自己的状态包括传感器读数、设备状态和错误码。#include cJSON.h char* generate_status_report(float temperature, float humidity, int battery_level, const char* firmware_version, bool is_overheating) { // 1. 创建一个空的JSON对象作为根 cJSON *root cJSON_CreateObject(); if (root NULL) { goto end; // 内存分配失败 } // 2. 向根对象中添加各种类型的键值对 // 添加字符串 if (!cJSON_AddStringToObject(root, device_id, sensor_abc123)) { goto end; } // 添加数字 (浮点) cJSON *temp_item cJSON_AddNumberToObject(root, temperature, temperature); // 可以给这个项添加一个注释仅对调试有用序列化时默认不包含 cJSON_AddStringToObject(temp_item, unit, Celsius); // 添加数字 (整数) cJSON_AddNumberToObject(root, humidity, humidity); cJSON_AddNumberToObject(root, battery, battery_level); // 添加布尔值 cJSON_AddBoolToObject(root, overheat, is_overheating); // 添加字符串 cJSON_AddStringToObject(root, fw_ver, firmware_version); // 3. 创建一个嵌套的对象比如GPS信息假设有 cJSON *gps_obj cJSON_CreateObject(); if (gps_obj ! NULL) { cJSON_AddNumberToObject(gps_obj, lat, 39.9042); cJSON_AddNumberToObject(gps_obj, lng, 116.4074); // 将嵌套对象添加到根对象中 cJSON_AddItemToObject(root, location, gps_obj); } // 如果创建失败可以选择跳过不添加location字段 // 4. 创建一个数组比如历史错误码 cJSON *error_array cJSON_CreateArray(); if (error_array ! NULL) { cJSON_AddItemToArray(error_array, cJSON_CreateNumber(0x01)); // 假设错误码1 cJSON_AddItemToArray(error_array, cJSON_CreateNumber(0x05)); // 假设错误码5 cJSON_AddItemToObject(root, recent_errors, error_array); } // 5. 将cJSON对象序列化为字符串紧凑格式节省带宽 char *json_string cJSON_PrintUnformatted(root); // 注意json_string 是动态分配的内存调用者需要负责释放 end: // 6. 无论成功与否都要删除cJSON对象树释放内存 cJSON_Delete(root); return json_string; // 如果生成失败这里会返回NULL } // 调用示例 void report_status() { float temp read_temperature(); float humi read_humidity(); int bat get_battery_level(); char* fw_ver v1.2.3; bool overheat check_overheat(); char *report generate_status_report(temp, humi, bat, fw_ver, overheat); if (report ! NULL) { // 通过Wi-Fi、NB-IoT等网络模块发送 report 字符串 // send_via_network(report); printf(Generated JSON: %s\n, report); // 例如{device_id:sensor_abc123,temperature:25.6,humidity:60.2,...} // 发送完成后务必释放字符串内存 free(report); } else { printf(Failed to generate status report.\n); } }实操心得错误处理cJSON_AddXXXToObject这类函数在内存分配失败时会返回NULL。在产品代码中你需要更严谨的错误处理而不是像我示例中简单地用goto end。一种策略是一旦添加失败就放弃整个对象的构建返回错误。内存管理cJSON_PrintUnformatted返回的字符串是使用malloc分配的调用者必须负责调用free()来释放它。这是新手最容易忘记导致内存泄漏的地方。同样cJSON_Delete负责释放整个cJSON对象树但它不负责释放cJSON_Print生成的字符串。添加NULL检查对于cJSON_CreateObject和cJSON_CreateArray创建失败也返回NULL需要检查。4. 进阶技巧与性能优化实战当你熟悉了基本操作后下面这些进阶技巧和优化策略能帮助你在真实项目中用得更好、更稳。4.1 使用流式解析器如jsmn处理大块数据假设你的设备通过4G模块接收一个很大的JSON配置文件有几十KB里面包含了上百个传感器的校准参数。用cJSON一次性解析可能会因为内存不足而失败。这时流式解析器jsmn就是救星。jsmn的原理是你预先分配一个固定大小的jsmntok_t令牌数组。解析时它遍历JSON字符串将每个关键元素如对象、数组、字符串、数字的起始位置、结束位置和类型记录到一个令牌中。解析完成后你得到的是一个令牌数组而不是一棵内存中的树。你需要自己编写代码根据令牌的类型和嵌套关系去原始JSON字符串中提取出你需要的数据。// 示例使用jsmn解析一个简单的JSON提取温度值 #include jsmn.h #include string.h int parse_temperature_with_jsmn(const char *json_str) { jsmn_parser p; jsmntok_t t[128]; // 预分配令牌数组大小需要预估要能容纳JSON中所有元素 int temperature -273; // 默认错误值 jsmn_init(p); int num_tokens jsmn_parse(p, json_str, strlen(json_str), t, sizeof(t)/sizeof(t[0])); if (num_tokens 0) { // 解析错误令牌数不足或JSON格式错误 return temperature; } // 假设JSON为{sensor: temp1, value: 23} // 令牌t[0]是整个对象t[1]是键sensort[2]是值temp1t[3]是键valuet[4]是值23 // 我们需要找到键value对应的令牌 for (int i 1; i num_tokens; i) { jsmntok_t *key_token t[i]; if (key_token-type JSMN_STRING) { // 计算键名长度并比较 int key_len key_token-end - key_token-start; if (strncmp(json_str key_token-start, value, key_len) 0) { // 下一个令牌就是对应的值 if (i 1 num_tokens) { jsmntok_t *value_token t[i 1]; if (value_token-type JSMN_PRIMITIVE) { // 从原始字符串中提取数字部分 char value_str[16]; int len value_token-end - value_token-start; strncpy(value_str, json_str value_token-start, len); value_str[len] \0; temperature atoi(value_str); // 转换为整数 } } break; } } } return temperature; }注意事项令牌数组大小必须足够大否则jsmn_parse会返回JSMN_ERROR_NOMEM。你可以先调用一次jsmn_parse并传入NULL作为令牌数组它会返回需要的令牌数量然后你再动态分配。但在嵌入式环境更常见的做法是根据最坏情况预估一个固定大小。手动提取所有数据提取工作都需要你手动完成代码量会比cJSON多但换来的是极低且可控的内存占用。只读jsmn只负责解析定位不提供修改或重新生成JSON的功能。适合“只读”场景。4.2 内存管理策略避免内存碎片与泄漏在长时间运行的嵌入式设备中内存泄漏和碎片是致命的。使用cJSON这类动态分配内存的库必须小心。使用自定义分配器cJSON允许你覆盖默认的malloc和free。你可以链接到你的内存池管理函数。// 你的内存池分配/释放函数 void* my_malloc(size_t size); void my_free(void* ptr); // 在程序初始化时告诉cJSON使用你的函数 cJSON_Hooks hooks {.malloc_fn my_malloc, .free_fn my_free}; cJSON_InitHooks(hooks);这样所有cJSON内部的内存分配都走你的内存池便于统一管理和监控。成对使用牢记cJSON_Parse配cJSON_DeletecJSON_Print返回的字符串配free。在复杂的函数中确保所有错误退出路径都正确释放了已分配的资源。限制JSON深度和大小在解析前可以先检查字符串长度如果超过预设阈值如1KB直接拒绝防止恶意或错误数据耗尽内存。cJSON本身不提供深度限制但你可以通过修改源码或在外层进行粗略控制。4.3 数据序列化优化减少传输开销JSON文本传输有冗余键名、引号、括号等。在带宽宝贵的NB-IoT、LoRa等场景需要优化。缩短键名这是最直接有效的方法。用t代替temperature用v代替value。可以建立一份设备与服务器约定的键名映射表。使用数字代替枚举字符串比如状态running用1表示error用2表示。使用数组代替对象当数据结构固定且有序时数组比对象更省空间。例如{t:25.6,h:60,p:1013}可以变成[25.6,60,1013]。但这牺牲了可读性需要双方严格约定每个位置的含义。考虑二进制编码对于极端带宽受限的场景可以在设备端生成JSON然后用CBORConcise Binary Object Representation或MessagePack等二进制格式进行编码再传输。接收方解码后再得到JSON。这需要在两端增加编解码库但能显著压缩数据体积。5. 常见问题排查与调试技巧在实际开发中你会遇到各种奇怪的问题。下面是我踩过的一些坑和解决方法。5.1 解析失败格式错误与内存不足问题现象cJSON_Parse返回NULL。排查步骤检查格式首先调用cJSON_GetErrorPtr()获取错误位置。最常见的错误是缺少引号、逗号或括号不匹配。将出错的JSON片段打印出来仔细看。一个隐藏陷阱确保你的JSON字符串是以\0结尾的C字符串。如果你从网络接收数据需要手动添加结束符。检查内存如果错误指针是NULL或者解析在很早就失败了很可能是内存分配失败malloc返回NULL。检查你的系统剩余内存。对于cJSON可以尝试解析一个非常简单的JSON如{}来确认是否是库本身的问题。检查编码确保JSON字符串是纯ASCII或UTF-8编码。如果包含中文字符等必须使用UTF-8。GBK等编码会导致解析异常。5.2 数据提取错误类型不匹配或空指针问题现象程序崩溃或读出的数据不对。排查步骤严格类型检查在访问item-valuestring或item-valueint前必须用cJSON_IsString,cJSON_IsNumber等函数判断。服务器下发的数据格式可能变动。检查键名是否存在cJSON_GetObjectItem在键不存在时返回NULL。直接对这个NULL指针调用类型判断函数或访问成员会导致崩溃。cJSON *item cJSON_GetObjectItem(root, my_key); if (item ! NULL cJSON_IsNumber(item)) { // 先判空再判类型 int val item-valueint; }字符串拷贝cJSON对象中的字符串指针 (valuestring) 直接指向原始JSON字符串的相应片段为了节省内存没有额外拷贝。这意味着如果你在解析后修改或释放了原始的JSON字符串缓冲区这些指针就会变成野指针。安全做法是如果需要长期保存字符串值应该用strdup或自己分配内存进行拷贝。5.3 性能瓶颈解析耗时与内存抖动问题现象设备响应变慢或运行一段时间后出现内存分配失败。排查与优化解析耗时对于频繁接收的小JSON解析开销可以接受。但对于大JSON解析时间可能占主导。优化方法一是使用更快的库评测显示parson有时比cJSON快二是使用jsmn这类流式解析器只提取必要字段三是让服务器简化数据。内存抖动频繁地创建、解析、删除JSON对象会导致内存碎片。优化方法复用对象如果JSON结构固定如状态上报可以创建一个全局的cJSON根对象每次上报只更新其中数值项的值而不是从头创建。最后用cJSON_Print生成字符串。注意更新字符串项时需要先free旧的valuestring再分配新的。使用内存池如前所述为cJSON配置自定义分配器使用固定大小的内存块避免碎片。避免频繁的小内存分配cJSON_Print生成的字符串是一次性分配的。如果频繁生成不同大小的报告考虑使用一个足够大的共享缓冲区来存放生成的JSON字符串。5.4 与服务器端的协作问题问题现象设备端解析正常但服务器说收不到数据或数据不对。排查技巧打印最终发送的字符串在调用网络发送函数前用printf或通过调试器查看cJSON_PrintUnformatted生成的字符串到底是什么。确认没有多余的字符如换行符确认编码正确。在线校验工具将设备生成的JSON字符串拷贝到在线的JSON校验工具如 JSONLint中检查格式是否绝对正确。协议头尾确保网络传输时只发送纯粹的JSON字符串没有额外添加长度头、校验和或特定的终止符除非协议要求。常见的错误是发送了json_str\n而不是json_str。内容协商HTTP POST请求时设置正确的Content-Type头为application/json。最后我个人最深刻的一个体会是在嵌入式项目引入JSON最大的挑战往往不是技术本身而是思维方式的转变。我们需要从“极致优化、自给自足”的单片机思维转向“开放互联、协作优先”的物联网思维。接受JSON那一点点“冗余”和“开销”换来的是开发速度的极大提升和系统扩展性的根本改善。当你第一次看到设备数据毫无障碍地显示在网页图表上或者被手机App轻松控制时你就会觉得这一切都是值得的。开始动手吧从一个简单的传感器数据上报做起JSON会成为你嵌入式工具箱里一件趁手的利器。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进