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,感謝您對本站的認可!
本文鏈接:http://www.newhan.cn/zhishi/a/1702128591240286.html
版權聲明:本站內容均來自互聯網,僅供演示用,請勿用于商業和其他非法用途。如果侵犯了您的權益請與我們聯系,我們將在24小時內刪除。
本文word下載地址:單片機串口實現字符串命令解析---使用函數指針(類似哈希表).doc
本文 PDF 下載地址:單片機串口實現字符串命令解析---使用函數指針(類似哈希表).pdf
| 留言與評論(共有 0 條評論) |