单片机使用 cJSON 开源库
Posted 嵌入式up笔记
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单片机使用 cJSON 开源库相关的知识,希望对你有一定的参考价值。
文章目录
单片机使用 cJSON 开源库
一、cJSON 介绍
21世纪初,Douglas Crockford 寻找一种简便的数据交换格式,方便在服务器之间交换数据。当时通用的数据交换语言是XML,但是 Douglas Crockford 觉得 XML 的生成和解析都太麻烦,所以他提出了一种简化格式,也就是JSON,JSON 是一种轻量级的数据交换格式,在短数据的情况下可以包含很多信息,比如如下格式的数据,就是 JSON 格式数据:
"城市":"北京",
"面积":16800,
"人口":1600
JSON 数据格式很容易懂,其数据存储在键值对中,然后数据和数据间逗号分隔,花括号保存对象,一个对象可以有多个数据,最后一个方括号保存多个对象,这就是 JSON
而 cJSON 则是一个开源的 JSON 解析器,用于解析 JSON 的数据,它是由纯 C 语言实现,所以叫 cJSON,该开源代码只有一个 cjson.c 和 cjson.h 文件,移植简单,跨平台性好,代码 Gitee 链接如下(OpenHarmony仓库整合了该开源代码,我放的整合的链接):Gitee cJSON 链接,本篇文章将简单分析一下 cJSON 的代码实现原理,然后再 STM32 单片机上使用 cJSON
cJSON 分析和使用过程中参考 Mculover666 大神的文章:cJSON使用详细教程 | 一个轻量级C语言JSON解析器
二、cJSON 源码分析
下载源码如下,最关键的是框选的那两个文件:
我将这两个文件添加到一个移植 LiteOS 和 LVGL 的 STM32工程里面
下面开始分析一下流程代码:
2.1 cJSON 数据结构
进入头文件看一下 cJSON 用于储存数据的数据结构:
/* The cJSON structure: */
typedef struct cJSON
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
cJSON;
cJSON 结构体成员含义如下:
- *next:链表的下一个节点
- *prev:链表的上一个节点
- *child:当前节点的一个嵌套子节点
- type:当前键值对的类型
- *valuestring:字符串键值对的首地址
- valueint:整形键值对的值
- cJSON_SetNumberValue:浮点型键值对的值
- *string:键值对名称
从 cJSON 的成员可以很明显的看出来 cJSON 存储的方式是一个双向链表,链表的每个节点都是一个键值对单元,就像下面一样,用链表的方法可以很容易对 JSON 键值对进行增删查改,十分方便:
"城市":"北京",
“:” 前面的 “城市” 是键值对的名称,":" 后面的 “北京” 则是键值对的值,这个值的类型是由 type 决定的,如果
type==cJSON_String and type == cJSON_Raw
那么就是 *valuestring 存放字符串的值,如果
type==cJSON_Number
则使用 valuedouble 存放浮点数值,至于整形直接使用 cJSON_SetNumberValue 函数来设置
结构体中还有一个 *child 节点,用于 JSON 对象或者数字嵌套
JSON 嵌套:
"城市":"北京",
"面积":16800,
"人口":1600,
"嵌套":
"名称":"值"
数组嵌套:
"城市":"北京",
"面积":16800,
"人口":1600,
"数组":["值1","值2","值3"]
2.2 cJSON 数据加载
2.2.1 数据对象
头文件里面有许多关于 cJSON 操作的函数,我们只需要关注其中一部分就行,先看一下数据加载相关的函数,第一个是创建一个 JSON 对象头结点,我们从上面可以了解到 cJSON 的数据是一个链表,首先我们要有一个头结点,头结点由我们自己创建,函数如下:
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void)
函数的源码就是调用 hooks->allocate 申请一个 cJSON 结构体长度的内存空间,然后返回指针强制转换为 cJSON 指针,创建完成后,将头结点的 type 初始化为:cJSON_Object,说明这是一个对象的头结点
2.2.2 对象数据加载
有了这个头结点,我们后面就可以调用其他 API 对他进行数据加载,其他 API 如下:
//添加一个NULL值的键值对,名称通过参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
//添加一个True值的键值对,名称通过参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
//添加一个False值的键值对,名称通过参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
//添加一个BOOL类型的键值对,名称通过参数传入,该BOOL类型的type值由传入的boolean控制,1为True,0位False
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
以上四个添加的都属于type类型,没有具体值,源码中直接设置 type 值来表示这三个值,设置完成后通过 add_item_to_object 函数,将配置好的该节点插入到头结点 child 指向的子节点末尾,插入过程如下:
第一次插入先判断头结点的 child 是不是为空,如果为空,插入节点1,节点关联如下,child->pre 指向自己(最后一个节点):
当再次插入节点时,会将 child->pre 指向节点的 next 指向新的节点2,然后将 2 的 pre 指向 child 节点的 pre,最后移动 child 节点的 pre 指向最新插入的节点(就是最末尾节点):
以上就是插入的大致流程
再看看另外几个 API:
//添加一个浮点值的键值对,名称通过参数传入,浮点值从参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
//添加一个字符串值的键值对,名称通过参数传入,字符串通过参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
添加浮点数和字符串代码流程与添加布尔类型代码流程差不多,只是在原先添加 type 的基础上,加入对应的值
//添加一个raw格式的键值对,名称通过参数传入,raw值参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
//添加一个新对象到目前对象下,名称和对象通过参数传入
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
//添加一个数组到目前对象下,名称和对象通过参数传
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
2.2.3 输出对象数据
当我们加载了数据之后,数据是存放在链表里面的,当我们加载完成后,就需要把数据输出成一串字符串,方便我们传递,输出成字符串函数主要用到下面两个:
//转化为字符串,带有格式
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
//转化为字符串,不带有格式
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
格式就是换行回车空格啥的修饰符
2.3 cJSON 数据解析
数据解析的步骤首先获取 JSON 数据字符串,创建一个 cJSON 对象,调用解析函数将字符串解析为 cJSON 链表,获取首地址,然后根据键值对的名称从链表中取出对应值,完成数据解析
2.3.1 解析函数
一般使用如下函数,传入 JSON 字符串,返回链表头指针
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
代码实现原理就是先简单预处理,跳过一些符号,然后逐个遍历,对边字符,填入到相应对象中去
2.3.2 链表查询
当链表解析完成后,我们使用如下接口,查询该链表中对应的参数值,string 传入的是需要查询的键值对名称
(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
查询成功后会返回该值的 cJSON 节点
2.3.3 嵌套对象和数组查询
除了查询键值对,还可以查询键值对数组和嵌套节点,函数如下:
//获取当前子节点的数组大小
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
//按照名称查找头节点指向的链表,返回该名称对应的数组节点
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
//按照名称查找头节点指向的链表,返回该名称对应的嵌套 cJSON 对象节点
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
//查找头节点指向的链表是否包含有嵌套对象节点
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
2.4 cJSON 动态内存分配
cJSON 代码使用动态内存分配,默认的使用 malloc 和 free,但很多应用场景中内存限制较高,无法使用,所以支持用户自定义动态内存分配函数:
首先用如下结构体定义一个函数
typedef struct cJSON_Hooks
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
cJSON_Hooks;
将定义的结构体两个成员函数换成我们自己的函数,然后使用
(void) cJSON_InitHooks(cJSON_Hooks* hooks);
初始化钩子函数,该钩子函数的源码实现原理,就是将用户自定义的函数传递给全局内存分配钩子函数,替换掉默认的内存分配函数
三、cJSON 使用
下面介绍一下在 STM32 上使用 cJSON 来加载和解析数据,此处我直接使用的小熊派,移植好 lvgl 和 liteos,内存分配使用的小熊派的内存分配方式,当然换成 liteos 的也没问题,怎么方便怎么来
3.1 内存分配替换
使用 lvgl 的内存分配来替换 malloc 和 free,lv_init() 是 lvgl 初始化
cJSON_Hooks my_mem;
lv_init();
my_mem.malloc_fn = lv_mem_alloc;
my_mem.free_fn = lv_mem_free;
cJSON_InitHooks(&my_mem);
3.2 加载数据
编写一段测试代码,含有两个键值对和一个嵌套的对象,也包含两个键值对:
//创建资源
cJSON* cjson_test1 = NULL;
cJSON* cjson_test2 = NULL;
char* str = (char*)lv_mem_alloc(100);
//添加一个对象,外加两个参数
cjson_test1 = cJSON_CreateObject();
cJSON_AddStringToObject(cjson_test1, "jeckxu666", "test_code");
cJSON_AddStringToObject(cjson_test1, "time", "2022-2-25");
//嵌套一个对象
cjson_test2 = cJSON_CreateObject();
cJSON_AddStringToObject(cjson_test2, "name", "jeckxu");
cJSON_AddNumberToObject(cjson_test2, "num", 666);
cJSON_AddItemToObject(cjson_test1, "嵌套", cjson_test2);
//无格式转换整个数据
printf("无格式转换数据:\\r\\n");
str = cJSON_PrintUnformatted(cjson_test1);
printf("%s\\r\\n", str);
//有格式转换整个数据
printf("有格式转换数据:\\r\\n");
str = cJSON_Print(cjson_test1);
printf("%s\\r\\n", str);
//回收资源
cJSON_Delete(cjson_test1);
cJSON_Delete(cjson_test2);
lv_mem_free(str);
烧写后,串口打印代码如下:
3.3 解析数据
拿这段数据做解析
char json_dat[]="\\"jeckxu666\\":\\"test_code\\",\\"time\\":\\"2022-2-25\\",\\"嵌套\\":\\"name\\":\\"jeckxu\\",\\"num\\":666\\";
解析代码如下,我们解析 jeckxu666 的值:
//添加一个头指针,用来解析
cJSON* cjson_test3 = NULL;
//解析到头指针
cjson_test3 = cJSON_Parse(json_dat);
//获取 jeckxu666 的节点
cJSON* jeckxu666 = cJSON_GetObjectItem(cjson_test3, "jeckxu666");
//打印解析的值
printf("%s\\r\\n",jeckxu666->valuestring);
烧写代码,得到数据:
以上是关于单片机使用 cJSON 开源库的主要内容,如果未能解决你的问题,请参考以下文章
cmake编译cJSON,使用时找不到cjson-static target(静态库) 的问题
C/C++ 使用cjson库 操作Json格式文件(创建插入解析修改删除)