提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
SON字符串在应用层面,如安卓、云平台,应用的十分广泛。对于高级语言的开发者来说比底层的十六进制开发要简单的多。因此很多安卓与单片机的协议也是基于JSON做的。对于ARM系列单片机来说做JSON的解析并非难事。只要使用CJSON库即可。通过一些接口的调用就可以实现JSON的解析和封装。不过需要注意的一点是这个库还是比较吃资源的,而且需要malloc来动态申请内存。相较于ARM动辄十几K,几十K的RAM来说,问题并不大。
但是随着这两年,芯片价格的变动。例如这几年的ST价格的剧烈波动。虽然带动了国产替代方案的迅速崛起。如:GD、APM等PINTOPIN替代方案。但是很多用户的目光又回到了51系的单片机。
虽然开发环境不友好、架构与ARM差别大、资源少但是架不住价格便宜的诱惑。这个年代做到几块钱的MCU还是吃香呀,并且现在51单片机,例如STC,速率、模拟性能、Flash容量也不差了,还有片上EEPROM,支持串口自定义命令的ISP升级,定时器资源等等。唯一的缺点就是内存太小。并且对于一些基于指针操作的C代码和函数也不友好。
那在系统整体降成本的过程中,如果想要保持协议不变。例如JSON的话呢,怎么办呢?
一、JSON解析库
引用现成的CJSON不合适,一个是并不需要全协议的支持。第二个再根据应用进行裁剪也费时费力。反倒不如直接重新简单做个小库来的方便。
整体小库大体可以分为三个部分:
1、串口协议的解析(大部分还是用的串口);
2、JSON的解析和封装;
3、循环队列QUEUE。
来实现一个资源占用较少的JSON协议解析。当然有利有弊,资源少了,带来的限制就多了,例如协议相对固定,然后队列的存储采用定长缓冲区为单位,没法做到内存使用的高效率。
二、具体实现
1.串口解析
代码如下(示例):
void vACLRcvDataHandle(uint8_t ucData, uint8_t ucOrder)
{
static uint16_t usLengthFlag = 0;
static uint8_t *pCmdLine = NULL;
static uint8_t ucACLHead[3] = {
0};
static uint16_t usCounter = 0;
static ACLRcvSta_e eACLRcvSta = eACLRcvStaIdle;
static uint8_t ucCheck = 0;
uint8_t ucTemp = ucData;
if(ucOrder == ACL_RCV_STA_CLR) {
eACLRcvSta = eACLRcvStaIdle;
return;
}
switch(eACLRcvSta)
{
case eACLRcvStaIdle: //serch head
ucACLHead[0] = ucACLHead[1];
ucACLHead[1] = ucACLHead[2];
ucACLHead[2] = ucTemp;
// 寻找帧头
if(memcmp(ucACLHead, GucACLHeadBuf, 3) == 0)
{
eACLRcvSta = eACLRcvStaType;
pCmdLine = GucACLRcvBuf;
ucACLHead[2] = 0x00;
}
break;
case eACLRcvStaType: //serch TYPE
// 数据类型判断
if((ucTemp == eACLRcvTypeNormal) || (ucTemp == eACLRcvTypeUpdate))
{
eACLRcvSta = eACLRcvStaLength;
// 长度标识
usLengthFlag = 0;
ucCheck = 0;
*pCmdLine = ucTemp;
pCmdLine++;
}
else
{
eACLRcvSta = eACLRcvStaIdle;
}
break;
case eACLRcvStaLength: //serch Length
usLengthFlag++;
*pCmdLine = ucTemp;
pCmdLine++;
ucCheck += ucTemp;
// 数据长度计算
if(usLengthFlag >= 2)
{
usLengthFlag = (GucACLRcvBuf[1] << 8) + GucACLRcvBuf[2];//计算数据长度,高位移位加低位
if( usLengthFlag >= 3) {
// 数据长度越界检测
if(usLengthFlag > ACL_COMM_MAX_SIZE ) {
eACLRcvSta = eACLRcvStaIdle;
} else {
usCounter = 0;
eACLRcvSta = eACLRcvStaRecving;
}
} else {
eACLRcvSta = eACLRcvStaIdle;
}
}
break;
case eACLRcvStaRecving: //push receive data to cmd line
//存数据
*pCmdLine = ucTemp;
pCmdLine++;
usCounter++;
ucCheck += ucTemp;
if(usCounter >= (usLengthFlag - 2)) {
eACLRcvSta = eACLRcvStaCheck;
}
break;
case eACLRcvStaCheck:
if(ACL_CHECK) {
if(ucCheck != ucTemp ) {
eACLRcvSta = eACLRcvStaIdle;
} else {
eACLRcvSta = eACLRcvStaEnd;
}
} else {
eACLRcvSta = eACLRcvStaEnd;
}
break;
case eACLRcvStaEnd:
*pCmdLine = '0'; // add string end flag
if(ucTemp == 0xFF)
{
eACLRcvSta = eACLRcvStaIdle;
vACLPortQueueSendJson(GucACLRcvBuf);
} else {
eACLRcvSta = eACLRcvStaIdle;
}
break;
default:
eACLRcvSta = eACLRcvStaIdle;
break;
}
}
这个根据各自的协议进行设计就行。这里的DEMO除了JSON串以外,增加了帧头帧尾 帧长度 校验位 帧类型。
解析方式基本类似于常用的AT指令的解析方式,相对来说简单可靠。解析完成后通过循环队列进行存储。队列的长度可以自己定义,资源多的可以定义的长一点。资源少的就定义的短一点。
最小的资源包含一个单位的串口接收缓冲区。一个单位的接收队列和一个单位的发送队列。如果单位长度为100字节的话,整个库最低消耗可能不到500个字节。对于51来说还是很友好的。
2.JSON的解析和封装
代码如下(示例):
static ACLBaseType prxJsonParse(uint8_t *pBuf,ACLOrder_s *psACLOrders)
{
uint8_t *cmd_json = NULL;
uint8_t *cmd_type = NULL;
uint8_t *cmd_var1 = NULL;
uint8_t *cmd_var2 = NULL;
// Init
psACLOrders->ucType = 0xFF;
psACLOrders->ulVar1 = 0xFF;
psACLOrders->ulVar2 = 0xFF;
cmd_json = pBuf;
// 判断是否存在JSON串
if(cmd_json[0] != '{')
{
return ACL_FALSE;
}
else
{
cmd_type = strstr(cmd_json, "type");
cmd_var1 = strstr(cmd_json, "var1");
cmd_var2 = strstr(cmd_json, "var2");
// Carefully !! atoi return 0 even if str has no number
if( !cmd_type)
{
return ACL_FALSE;
}
else
{
psACLOrders->ucType = atoi(cmd_type+6);
}
if(cmd_var1)
{
psACLOrders->ulVar1 = atoi(cmd_var1+6);
}
if(cmd_var2)
{
psACLOrders->ulVar2 = atoi(cmd_var2+6);
}
}
return ACL_TRUE;
}
static ACLBaseType vACLSendJsonConcatenation( ACLSendType_e eACLSendType, ACLOrder_s *pACLOrder, char *pcBuffer, uint16_t *pusLen)
{
uint8_t *pPoint = pcBuffer;
BUFFER_LOCAL uint8_t ucTempBuf[ACL_VAR2_BUF_SIZE+7] = {
0};
uint8_t ucTemp = 0;
char *pcString = NULL;
if(((POINTE_CAST)pPoint == NULL)||((POINTE_CAST)pACLOrder == NULL)) {
return ACL_FALSE;
}
/* 创建JSON根节点 */
pPoint[0] = '{';
pPoint++;
// Add type
ucTemp = sprintf(ucTempBuf,"\"type\":%d,",(uint16_t)pACLOrder->ucType);
memcpy(pPoint,ucTempBuf,ucTemp);
pPoint += ucTemp;
switch(eACLSendType)
{
case eACLSendVar2None:
pACLOrder->ulVar2 = 0;
case eACLSendVar2Int:
{
ucTemp = sprintf(ucTempBuf,"\"var1\":%d,",(uint16_t)pACLOrder->ulVar1);
memcpy(pPoint,ucTempBuf,ucTemp);
pPoint += ucTemp;
ucTemp = sprintf(ucTempBuf,"\"var2\":%d",(uint16_t)pACLOrder->ulVar2);
memcpy(pPoint,ucTempBuf,ucTemp);
pPoint += ucTemp;
break;
}
case eACLSendVar2Str:
{
ucTemp = sprintf(ucTempBuf,"\"var1\":%d,",(uint16_t)pACLOrder->ulVar1);
memcpy(pPoint,ucTempBuf,ucTemp);
pPoint += ucTemp;
ucTemp = sprintf(ucTempBuf,"\"var2\":");
memcpy(pPoint,ucTempBuf,ucTemp);
pPoint += ucTemp;
memcpy(pPoint,pACLOrder->ucVar2Buf,strlen(pACLOrder->ucVar2Buf)%(ACL_VAR2_BUF_SIZE+1));
pPoint += strlen(pACLOrder->ucVar2Buf)%(ACL_VAR2_BUF_SIZE+1);
break;
}
default:
{
break;
}
}
*(pPoint++) = '}';
*pusLen = (uint16_t)(pPoint - pcBuffer);
return ACL_TRUE;
}
解析和封装也是要根据自己的协议进行定义。基本的就使用了标准库函数里面的接口。相对也比较简单 strstr atoi memcpy sprintf。没有什么难度,虽然引用库函数,但是效率上也是比较快的。一般的应用没什么大的差别。
3.循环队列QUEUE
extern INT8U queueCreate( DATAQUEUE *Queue,
void *pBuf,
INT32U ulSizeOfBuf,
INT8U (* pfuncReadEmpty)(),
INT8U (* pfuncWriteFull)()
);
extern INT8U queueReadNData(DATAQUEUE *Queue, uint8_t *pucDestBuf, uint8_t ucNumber) ;
extern INT8U queueWriteNData(DATAQUEUE *Queue, uint8_t *pucSrcBuf, uint8_t ucNumber);
queue的部分各自都有自己喜欢的或者常用的queue库。只要实现好queue初始化、入队出队就可以了。
提示一下临界区保护还是一定要有的。
总结
这样子就简单实现了一个简单的JSON解析。整个库也非常透明,基本没有封装的很深的部分。也比较简单,根据自己的协议搞搞,1天左右也就弄完了。
并且由于循环队列的存在,也支持了命令的缓冲,减少命令的丢失。
注:文章是作者原创,同时发布于:Z站 https://z.zlg.cn/articleinfo?id=852836