单片机使用 cJSON 开源库

文章目录

  • 单片机使用 cJSON 开源库
    • 一、cJSON 介绍
    • 二、cJSON 源码分析
      • 2.1 cJSON 数据结构
      • 2.2 cJSON 数据加载
      • 2.3 cJSON 数据解析
      • 2.4 cJSON 动态内存分配
    • 三、cJSON 使用
      • 3.1 内存分配替换
      • 3.2 加载数据
      • 3.3 解析数据

单片机使用 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 源码分析

下载源码如下,最关键的是框选的那两个文件:

单片机使用 cJSON 开源库_第1张图片

我将这两个文件添加到一个移植 LiteOS 和 LVGL 的 STM32工程里面

单片机使用 cJSON 开源库_第2张图片

下面开始分析一下流程代码:

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 指向自己(最后一个节点):

单片机使用 cJSON 开源库_第3张图片

当再次插入节点时,会将 child->pre 指向节点的 next 指向新的节点2,然后将 2 的 pre 指向 child 节点的 pre,最后移动 child 节点的 pre 指向最新插入的节点(就是最末尾节点):

单片机使用 cJSON 开源库_第4张图片

以上就是插入的大致流程

再看看另外几个 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);

烧写后,串口打印代码如下:

单片机使用 cJSON 开源库_第5张图片

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 开源库_第6张图片

你可能感兴趣的:(GitHub开源项目体验,单片机,嵌入式硬件,json,物联网,开源项目)