单片机串口实现字符串命令解析---使用函数指针(类似哈希表)

更新时间:2023-12-09 21:29:51 阅读: 评论:0

2023年12月9日发(作者:屈平)

-

单片机串口实现字符串命令解析---使用函数指针(类似哈希表)

单片机串口实现字符串命令解析---使用函数指针(类似哈希

表)

通常情况下串口通信用的大多数都是用十六进制数据来传输指令,比如最常见的modbus的通信,如读保持寄存器指令:01 03 00 00

00 01 84 0A,这种十六进制的指令在这里就不讨论了。想要详细了解可以看往期的文章。串口相关文章链接如下:

有时候串口也需要通过字符串命令来实现某些功能, 如果使用AT指令的话,通信就是以字符串格式进行。

有时候自己做产品的时候,需要通过指令来控制设备,使用字符串的话更方便。比如发送一条开LED灯的指令"led on",关灯指令"led

off"。这样通过字符串命令来控制设备,比直接通过16进制数字控制设备看起来更方便直观。比如今天要实现的功能。

那么如何解析字符串命令呢?通常第一个想法就是,将读取到的字符串和程序中存储的字符串进行比较,如果字符串相等的话,就调

用某个函数去执行相应的功能。这种办法是最简单也就是最容易实现的。通常用一个switch语句就可以搞定,switch的每个分支代表一个指

令,然后去调用相关的函数执行。相关代码可以参考 这篇文章。

还有没有其他方法呢?如果了解数据结构的话就可以想到哈希表。可以使用类似于哈希表的原理来实现。在一个表中每一个键就对应

一个值。通过键就可以找到值。就好像学生的学号和姓名一样,将学号和姓名连接起来,放在一起,通过学号就可以找到姓名。将字符串和

要执行的函数对应起来,放在哈希表中。在表中找到字符串后,就可以直接找到对应执行的函数。增加或者删除串口命令时,只需要操作这

张表就行了。就不用每次指令或者函数名发生变化时,都要去switch语句中修改。

在单片机中没有类似哈希表的这种数据结构,那要怎么实现呢?于是想到了用结构体去实现,在一个结构体里面有两个元素,一个是字

符串,一个是需要执行的函数。这样字符串和函数就被绑定在了一起。在初始化命令的时候,将字符串和对应的函数,都写在一个结构体

中。那么只有找到了这个字符串,就自然会找到对应的执行函数。这样就实现了类似哈希表的功能。

首先定义一个结构体,用来存储字符串命令和对应功能的函数

typedef void ( *functions )( void ); // 函数指针类型

//命令结构体

typedef struct

{

char cmd_name[MAX_CMD_LENGTH + 1]; //字符数组存储字符串命令

functions cmd_functions; //通过指针传递函数名

}CMD_Name_Func;

在结构体里面有两个元素,一个字符数组用来存储字符串命令。一个指针用来存储函数的入口地址,该函数没有返回值,没有参数。

如果每个命令都对应一个这样的结构体,命令比较多的时候,如何能方便快速去找到这些结构体呢?最简单的就是将这些结构体存储在

数组中,这样数组的每一个元素就是一个结构体,通过数组的下标就能很方便的访问到每一个结构体。

// 命令列表结构体类型

typedef struct

{

CMD_Name_Func cmdNames[MAX_CMDS_COUNT]; //结构体数组字符串命令 和对应函数

int num; //统计结构体个数

}CMD_LIST;

在另一个结构体中用一个数组来存储命令结构体,每个结构体的数组元素都代表一个字符串命令和对应的函数,同时用一个计数器来

统计,共存储了多少条命令。当串口接收到一个字符串后,就遍历一次结构体数组,对比里面是否有相同的字符串,如果有,就执行该字符

串对应的函数。通过这种方式来处理字符串和命令的话,只需要在初始化的时候将字符串命令添加到这个列表中就可以了,而程序的其他地

方就不需要修改了。

要实现上面所说的功能,还需要再实现两个函数,一个函数实现将命令添加到结构体,一个函数实现遍历结构体数组,寻找匹配的字符

串并执行相应的函数。

static CMD_LIST command_list = {NULL, 0}; // 全局命令列表,保存已注册命令集合

//注册命令

void register_cmds( CMD_Name_Func reg_cmds[], int length )

{

int i;

if ( length > MAX_CMDS_COUNT )

{

return;

}

for ( i = 0; i < length; i++ )

{

if ( command_ < MAX_CMDS_COUNT ) // 命令列表未满

{

strcpy( command_es[command_].cmd_name, reg_cmds[i].cmd_name ); //将字符串命令拷贝到列表中

command_es[command_].cmd_functions = reg_cmds[i].cmd_functions; //将命令对应的函数存储在列表中

command_++; // 数量值默认为0,每添加一个命令,数量加1.

}

}

}

这个命令注册函数实现将命令结构体添加到命令列表中。用户新增加一条指令,就调用一次注册函数,将字符串命令添加到命令列表

字符串中,同时将字符串命令对应的函数也添加到列表函数中。如果有新增加的子模块,只需要在子模块中调用一次注册命令,就完成了字

符串命令的增加。其他代码不需要修改。

比如现在led模块需要添加命令

//注册led命令

void led_register( void )

{

//初始化 字符串命令和对应函数

CMD_Name_Func led_cmds[] =

{

{"led1 on", led1_on}, // 添加字符串命令 和 对应的函数

{"led1 off", led1_off},

{"led2 on", led2_on},

{"led2 off", led2_off},

{"led3 on", led3_on},

{"led3 off", led3_off}

};

//将命令添加到列表中

register_cmds( led_cmds, ARRAY_SIZE( led_cmds ) ); // ARRAY_SIZE 用来计算结构体数组中,数组的个数。个数=结构体总长度/单个数组长度

}

在led模块中创建命令结构体,并将创建的结构体添加到命令列表中。通过代码可以看到增加了6条关于led的字符串命令,每个字符串

命令都对应一个需要执行的函数。

假如现在还需要添加一个蜂鸣器的子模块,那么就可以直接在蜂鸣器的子文件内直接注册命令。

//注册 beep命令

void beep_register( void )

{

//初始化 字符串命令和对应函数

CMD_Name_Func beep_cmds[] =

{

{"beep on", beep_on},

{"beep off", beep_off}

};

//将命令添加到列表中

register_cmds( beep_cmds, ARRAY_SIZE( beep_cmds ) ); // ARRAY_SIZE 用来计算结构体数组中,数组的个数。个数=结构体总长度/单个数组长度

}

在蜂鸣器的模块中添加了两条命令,然后通过注册函数将蜂鸣器相关的命令就添加到了命令列表中。

通过一个注册命令就实现了命令的添加,而不需要修改其他的代码,实现了代码的"

高内聚

低耦合"。

上面实现了命令的注册,还需要

实现一个命令的解析。

void match_cmd( char *cmdStr )

{

int i;

if ( strlen( cmdStr ) > MAX_CMD_LENGTH )

{

return;

}

for ( i = 0; i < command_; i++ ) // 遍历命令列表

{

{

command_es[i].cmd_functions();

}

}

}

if ( strcmp( command_es[i].cmd_name, cmdStr ) == 0 ) //比较接收到的命令字符串 和 列表中存储的命令字符串是否相等,如果相等就调用命令字

每次注册命令的时候,会有个计数器统计注册命令的数量。在命令解析函数中就循环去判断接收到的命令是否在命令列表中,如果命

令列表中存在相等的字符串,就去执行对应的函数。而命令解析函数是不关心接收到的具体字符串命令是什么,需要执行的相应函数是什

么。所以每次命令添加或者删除的时候,对命令解析和函数没有任何的影响。

这个命令解析函数比较类似于设计模式中的"工厂模式",所谓的工厂模式百度百科解释如下:

如果不了解面向对象编程的话,可能上面的这个解释看的不太明白。举个简单的例子就是,工厂生产东西的时候不关心具体生产的是

什么东西,客户将需要生产东西的大小尺寸颜色特征告诉工厂,工厂就按照要求去执行。比如客户要求做一个直径5cm的玻璃透明圆柱体,

圆柱体只需要底面,不需要顶面。工厂就按照客户的要求去生产这样一个东西,虽然这个东西按照一般经验来看就是一个透明的玻璃杯。但

是工厂不用关心这个东西的名称和用途,只需要按照客户的要求去实现。

而上面的命令解析函数,实际上也就是一个工厂,客户将一个字符串和一个函数送来。工厂就按照指定的字符串去执行指定函数。而工

厂本身不去关心这个字符串具体是什么?函数具体是什么?这样的话,只要客户在命令列表中注册了字符串命令和相应的执行动作。命令解

析函数就可以实现想要的功能。

通过这种模式去解析字符串命令的话,就可以移植到到任何需要命令解析的单片机上,而不用去关心单片机的IO、中断、寄存器等等其

他东西。下面就贴出完整的代码

命令解析头文件 cmd.h :

#ifndef __CMD_H_

#define __CMD_H_

#include "iostm8s103F3.h"

#define ARRAY_SIZE(x) (sizeof(x) / (sizeof((x)[0]))) //用来计算结构体数组中,数组的个数。个数=结构体总长度/单个数组长度

#define MAX_CMD_LENGTH 15 // 最大命令名长度

#define MAX_CMDS_COUNT 20 // 最大命令数

typedef void ( *functions )( void ); // 命令操作函数指针类型

//命令结构体类型 用于存储字符串命令和对应函数

typedef struct

{

char cmd_name[MAX_CMD_LENGTH + 1]; // 命令名 字符串末尾系统会自动添加结束符'/0' sizeof("name")大小为 10

functions cmd_functions; // 命令操作函数 sizeof(func) 大小为 2

}CMD_Name_Func;

// 命令列表结构体类型 用于存储字符串命令数组

typedef struct

{

CMD_Name_Func cmdNames[MAX_CMDS_COUNT]; // 存储字符串命令 和对应函数

int num; // 命令数组个数

}CMD_LIST;

void register_cmds( CMD_Name_Func reg_cmds[], int num );

void match_cmd( char *str );

#endif

命令解析代码cmd.c

#include

#include "cmd.h"

#include "uart.h"

static CMD_LIST command_list = {NULL, 0}; // 全局命令列表,保存已注册命令集合

/*

* 函数介绍: 命令注册函数 每新添加一个命令,就添加到命令列表中

* 输入参数: reg_cmds 待注册命令结构体数组

* length 数组个数

* 输出参数: 无

* 返回值 : 无

* 备 注: length 不得超过 MAX_CMDS_COUNT

*/

void register_cmds( CMD_Name_Func reg_cmds[], int length )

{

int i;

if ( length > MAX_CMDS_COUNT )

{

return;

}

for ( i = 0; i < length; i++ )

{

if ( command_ < MAX_CMDS_COUNT ) // 命令列表未满

{

strcpy( command_es[command_].cmd_name, reg_cmds[i].cmd_name ); //将字符串命令拷贝到列表中

command_es[command_].cmd_functions = reg_cmds[i].cmd_functions; //将命令对应的函数存储在列表中

command_++; // 数量值默认为0,每添加一个命令,数量加1.

}

}

}

/*

* 函数介绍: 命令匹配执行函数

* 输入参数: cmdStr 待匹配命令字符串

* 输出参数: 无

* 返回值 : 无

* 备 注: cmdStr 长度不得超过 MAX_CMD_NAME_LENGTH

*/

void match_cmd( char *cmdStr )

{

int i;

if ( strlen( cmdStr ) > MAX_CMD_LENGTH )

{

return;

}

for ( i = 0; i < command_; i++ ) // 遍历命令列表

{

{

command_es[i].cmd_functions();

}

}

}

if ( strcmp( command_es[i].cmd_name, cmdStr ) == 0 ) //比较接收到的命令字符串 和 列表中存储的命令字符串是否相等,如果相

ed头文件led.h

#ifndef __LED_H

#define __LED_H

#include "iostm8s103F3.h"

#define LED1 PD_ODR_ODR4 //蓝

#define LED2 PA_ODR_ODR1 //绿

#define LED3 PA_ODR_ODR2 //红

#define BLUE {LED1=1;LED2=0;LED3=0;}

#define GREEN {LED1=0;LED2=1;LED3=0;}

#define RED {LED1=0;LED2=0;LED3=1;}

#define CYAN {LED1=1;LED2=1;LED3=0;} //青

#define PURPLE {LED1=1;LED2=0;LED3=1;} //紫

#define YELLOW {LED1=0;LED2=1;LED3=1;} //黄

#define ONALL {LED2=1;LED3=1;LED1=1;}

#define OFFALL {LED1=0;LED2=0;LED3=0;}

void LED_GPIO_Init( void );

void led1_on(void);

void led1_off(void);

void led2_on(void);

void led2_off(void);

void led3_on(void);

void led3_off(void);

void led_register(void);

#endif

led.c

#include "led.h"

#include "cmd.h"

//3色LED

void LED_GPIO_Init( void )

{

PD_DDR |= ( 1 << 4 ); //PD4 输出 led

PD_CR1 |= ( 1 << 4 ); //PD4 推挽输出

PD_CR2 |= ( 1 << 4 );

PA_DDR |= ( 1 << 1 ); //PA1 输出 led

PA_CR1 |= ( 1 << 1 ); //PA1 推挽输出

PA_CR2 |= ( 1 << 1 );

PA_DDR |= ( 1 << 2 ); //PA2 输出 led

PA_CR1 |= ( 1 << 2 ); //PA2 推挽输出

PA_CR2 |= ( 1 << 2 );

}

void led1_on( void )

{

LED1 = 1;

}

void led1_off( void )

{

LED1 = 0;

}

void led2_on( void )

{

LED2 = 1;

}

void led2_off( void )

{

LED2 = 0;

LED2 = 0;

}

void led3_on( void )

{

LED3 = 1;

}

void led3_off( void )

{

LED3 = 0;

}

//注册led命令

void led_register( void )

{

//初始化 字符串命令和对应函数

CMD_Name_Func led_cmds[] =

{

{"led1 on", led1_on}, // 一个结构体变量大小为 12 (字符串大小10 + 函数名大小2)

{"led1 off", led1_off}, // 一个结构体变量大小为 12

{"led2 on", led2_on},

{"led2 off", led2_off},

{"led3 on", led3_on},

{"led3 off", led3_off}

};

//将命令添加到列表中

register_cmds( led_cmds, ARRAY_SIZE( led_cmds ) ); // ARRAY_SIZE 用来计算结构体数组中,数组的个数。个数=结构体总长度/单个数组长度

}

beep.h

#ifndef __BEEP_H

#define __BEEP_H

#include "iostm8s103F3.h"

#define BEEP PB_ODR_ODR4

void BEEP_GPIO_Init( void );

void beep_register( void );

#endif

beep.c

#include "beep.h"

#include "cmd.h"

void BEEP_GPIO_Init( void )

{

PB_DDR |= ( 1 << 4 ); //PB4

PB_CR1 |= ( 1 << 4 ); //PB4 推挽输出

PB_CR2 |= ( 1 << 4 );

}

void beep_on( void )

{

BEEP = 1;

}

void beep_off( void )

{

BEEP = 0;

}

//注册 beep命令

void beep_register( void )

{

//初始化 字符串命令和对应函数

CMD_Name_Func beep_cmds[] =

{

{"beep on", beep_on}, // 一个结构体变量大小为 12 (字符串大小10 + 函数名大小2)

{"beep off", beep_off} // 一个结构体变量大小为 12

};

//将命令添加到列表中

register_cmds( beep_cmds, ARRAY_SIZE( beep_cmds ) ); // ARRAY_SIZE 用来计算结构体数组中,数组的个数。个数=结构体总长度/单个数组长度

}

uart.h

#ifndef __UART_H

#define __UART_H

#include "iostm8s103F3.h"

extern char uartRecStr[20]; //串口接收字符串存储

extern unsigned char uartRecCnt; //接收数据个数

extern _Bool rec_ok;

void Uart1_IO_Init( void );

void Uart1_Init( unsigned int baudrate );

void SendChar( unsigned char dat );

void SendString( unsigned char* s );

#endif

uart.c

#include "uart.h"

#include "main.h"

char uartRecStr[20] = {0}; //串口接收字符串存储

unsigned char uartRecCnt = 0; //接收数据个数

_Bool rec_ok = 0; //接收完成标志位

//在Library Options中将Printf formatter改成Large

//重新定向putchar函数,使支持printf函数

int putchar( int ch )

{

while( !( UART1_SR & 0X80 ) ); //循环发送,直到发送完毕

while( !( UART1_SR & 0X80 ) ); //循环发送,直到发送完毕

UART1_DR = ( u8 ) ch;

return ch;

}

//串口只用发送口,不用接收口

void Uart1_IO_Init( void )

{

PD_DDR |= ( 1 << 5 ); //输出模式 TXD

PD_CR1 |= ( 1 << 5 ); //推挽输出

PD_DDR &= ~( 1 << 6 ); //输入模式 RXD

PD_CR1 &= ~( 1 << 6 ); //浮空输入

}

//波特率最大可以设置为38400

void Uart1_Init( unsigned int baudrate )

{

unsigned int baud;

baud = 16000000 / baudrate;

Uart1_IO_Init();

UART1_CR1 = 0; //禁止发送和接收

UART1_CR2 = 0; //8 bit

UART1_CR3 = 0; //1 stop

UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) );

UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) );

UART1_CR2_ = 1; //接收使能

UART1_CR2_ = 1; //发送使能

UART1_CR2_ = 1; //接收中断使能

}

//阻塞式发送函数

void SendChar( unsigned char dat )

{

while( ( UART1_SR & 0x80 ) == 0x00 ); //发送数据寄存器空

UART1_DR = dat;

}

//发送字符串

void SendString( unsigned char* s )

{

while( 0 != *s )

{

SendChar( *s );

s++;

}

}

//接收中断函数 中断号18

#pragma vector = 20 // IAR中的中断号,要在STVD中的中断号上加2

__interrupt void UART1_Handle( void )

{

unsigned char res = 0;

res = UART1_DR;

UART1_SR &= ~( 1 << 5 ); //RXNE 清零

//SendChar(res); //test

if( ( res != 'r' ) && ( res != 'n' ) ) //字符串以回车换行符结束

{

uartRecStr[uartRecCnt++] = res;

}

el

{

rec_ok = 1; //置接收完成标志

}

}

主程序main.c

/*

*函数功能,实现串口字符串命令解析

*/

#include "iostm8s103F3.h"

#include "main.h"

#include "stdio.h"

#include "delay.h"

#include "stdlib.h"

#include "uart.h"

#include "string.h"

#include "cmd.h"

#include "led.h"

#include "beep.h"

void SysClkInit( void )

{

CLK_SWR = 0xe1; //HSI为主时钟源 16MHz CPU时钟频率

CLK_CKDIVR = 0x00; //CPU时钟0分频,系统时钟0分频

}

void main( void )

{

__asm( "sim" ); //禁止中断

SysClkInit();

delay_init( 16 );

LED_GPIO_Init();

BEEP_GPIO_Init();

Uart1_Init( 9600 );

__asm( "rim" ); //开启中断

//注册命令

led_register();

beep_register();

while( 1 )

{

if( rec_ok )

{

rec_ok = 0;

uartRecCnt = 0;

SendString( uartRecStr );

SendString( "rn" );

match_cmd( uartRecStr );

memt( uartRecStr, 0, sizeof( uartRecStr ) ); //清空备份数组 需要添加头文件 string.h

}

}

}

在主函数中检测串口是否接收到了字符串,串口接收字符串以回车换行结束。若串口接收到了字符串,将接收到的字符串通过串口发

送出去,并检查一次接收到的字符串是否和命令列表中的字符串匹配?如果接收到的字符串和命令列表中的字符串匹配,就中执行一次相关

的函数。最后将串口缓冲区清空,继续等待下一次命令。

测试效果如下

这样通过字符串命令就可以直接控制LED灯和蜂鸣器了,如果下次需要增加一个继电器控制模块,就只需要编写继电器模块的c代码,

在进入main函数时,注册继电器命令。继电器的模块就会被添加进来了。而不需要修改其他的模块和串口任何代码。

通过上面的例子可以看到这种模式是相当的好用,难道这种方法就没有一点缺点吗?如果在单片机上用的话,这种模式有一个致命的

缺点,那就是太占内存了。

首先看一张图

这个是新建了4个led命令结构体,可以看出来每个命令的字符串数组长度都是16,函数指针默认为int型,占两个字节。一个结构体总

共占18个字节。 4个led命令占4*18=72个字节的空间。虽然led命令最长的字符串只占8个字节,但是系统依然会分配16个字节空间。

这个是命令列表,默认的最多命令数是20个,系统初始化的时候就一次性将这20个命令的空间分配好了。虽然代码中只用了4个命

令,但是系统空间的占用却一点也没有少。

这样的话对于空间比较小的单片机来说,虽然这种方法好用,但是太浪费空间。如果指令比较多,或者指令名比较长的话,可能光是

指令列表就会把单片机的内存占满。这样的话还不如直接在switch语句中去比较字符串,直接比较字符串的话,只需要开辟一个字符串的存

储空间就可以满足需求了。

所以根据不同的情况选择合适的方法,适合自己的方法就是好方法。

-

单片机串口实现字符串命令解析---使用函数指针(类似哈希表)

本文发布于:2023-12-09 21:29:51,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/zhishi/a/1702128591240286.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:单片机串口实现字符串命令解析---使用函数指针(类似哈希表).doc

本文 PDF 下载地址:单片机串口实现字符串命令解析---使用函数指针(类似哈希表).pdf

下一篇:返回列表
标签:命令   字符串   函数   结构   数组   需要   列表
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 实用文体写作网旗下知识大全大全栏目是一个全百科类宝库! 优秀范文|法律文书|专利查询|