市场上有很多的模块,比如蓝牙,WIFI,NB模块通常都是采用AT指令来与之通信,但是每个模块的AT指令不太一样,每个模块的每条指令又不太一样,所以做一个通用的模板,这个模板主要可以实现:

1.无操作系统实时性,处理时不阻塞其他代码的运行
2.可扩展性、移植性、复用性比较好

实时性是因为,可以将需要延时等待的部分分割出来,然后等待超时或者等待到标志才继续运行下面的步骤,有点操作系统里等待信号量的意思

单片机 通讯模块消息队列 单片机通信模块功能_单片机 通讯模块消息队列


第二点是因为使用表来保存AT指令列表,有点面向对象的意思

下面就用stm32 + hal库 + 蓝牙模块来做具体做一遍,用到的知识有:
C语言:关键字static、const,函数指针,void指针,结构体、联合体,位域,堆内存
数据结构:表,链表,消息队列
查找算法:顺序查找
编程思想:多线程,面向对象

具体过程为

1.建AT指令表
2.写AT指令处理过程
3.写给其他程序调用的接口函数

下面就分步来说

1.建AT指令表

使用结构体或者xmacro来建表,两种方法不太一样,不一样的地方可能就是这部分的写法不一样,但是和后面几点是不耦合的,示例用结构体建表
在 ble.h 创建AT指令所需要的参数

/*
	ble指令的结构体
*/
typedef struct
{
	uint8_t at_num;		/*编号*/
	uint8_t resend;		/*重发次数*/
	uint16_t at_time;	/*指令之间等待时间*/
	uint16_t time_out;	/*超时时间*/
	char *at_str;		/*发送指令*/
	char *recv_str;		/*期待回复值*/
	uint8_t (*at_send_callback)(uint8_t at_index,void *param);	/*回调函数*/
	uint8_t (*at_recv_callback)(uint8_t at_index,void *param);	/*回调函数*/
	
}BLEATStruct;

在ble.c创建结构体数组,也就是AT指令表,并写好相关的函数

BLEDataStruct ble_data = {0};
BLEProcessStruct ble_main = {0};
BLEParamStruct ble_param = {0};

static uint8_t ble_at_test(uint8_t at_index,void *param);
static uint8_t ble_at_test_recv(uint8_t at_index,void *param);
static uint8_t ble_at_broadcast_time(uint8_t at_index,void *param);
static uint8_t ble_at_config_name(uint8_t at_index,void *param);


/*
	指令列表
	需要添加新的指令步骤
	1.在下列的表添加信息
	2.写发送函数
	3.如果有需要,写接收函数
	4.调用
*/
static const BLEATStruct ble_at_table[] = 
{
	/*编号	重发次数	指令间隔	超时时间	发送字符		返回字符			发送回调				接收回调		*/
	{ 0 ,	5,			100,		1000,		"AT",			"OK"	,			ble_at_test,				NULL		},
	{ 1 ,	5,			100,		1000,		"AT+AINT=",		"OK+AINT"	,		ble_at_broadcast_time,	ble_at_test_recv},
	{ 2 ,	5,			100,		1000,		"AT+NAME=",		"OK+NAME"	,		ble_at_config_name,		ble_at_test_recv},
};


/*
	AT指令测试
*/
static uint8_t ble_at_test(uint8_t at_index,void *param)
{
	/*复制字符到待发送区*/
	add_data_to_send_buf((uint8_t *)ble_at_table[at_index].at_str,strlen(ble_at_table[at_index].at_str));
	
	return 1;
}

/*
	AT指令测试回复
*/
static uint8_t ble_at_test_recv(uint8_t at_index,void *param)
{
	printf("配置成功\r\n");
	return 1;
}

/*
	配置广播时间间隔
*/
static uint8_t ble_at_broadcast_time(uint8_t at_index,void *param)
{
	char temp[32] = {0};
	uint16_t *time = (uint16_t *)param;
	
	add_data_to_send_buf((uint8_t *)ble_at_table[at_index].at_str,strlen(ble_at_table[at_index].at_str));
	
	sprintf(temp,"%d",*time);
	
	add_data_to_send_buf((uint8_t *)temp,strlen(temp));
	
	return 1;
	
}

/*
	配置模块的名字
*/
static uint8_t ble_at_config_name(uint8_t at_index,void *param)
{
	char temp[16] = {0};
	
	char *name = (char *)param;
	
	add_data_to_send_buf((uint8_t *)ble_at_table[at_index].at_str,strlen(ble_at_table[at_index].at_str));
	
	sprintf(temp,"%s",name);
	
	add_data_to_send_buf((uint8_t *)temp,strlen(temp));
	
	return 1;
}

如果需要对指令增删改查的话,可以直接在这个表里面操作

2.写AT指令处理过程

接下来就是需要做整个AT指令的处理过程了,大概的流程就是

单片机 通讯模块消息队列 单片机通信模块功能_命令模式_02


这个处理过程主要就是把整个AT指令的发送,等待过程分割开来,就不会阻塞其他任务的运行了,有两种方法做,一种是用switch,一种也是建表,用指针函数来执行对应的步骤,还是用表来做

ble.h,创建两个结构体,一个是步骤表结构体,一个是步骤状态结构体

/*自己的进程状态*/
typedef struct
{
	union{
		uint8_t allbit;
		struct{
			uint8_t ready:1;
			uint8_t running:1;
			uint8_t suspend:1;
			uint8_t lock:1;
		}bits;
	}state;
	uint8_t step;
	uint8_t resend_cnt;		/*重发次数*/
	uint32_t timer;	/*定时器*/
	
}BLEProcessStruct;

/*
	步骤表结构体
*/
typedef struct
{
	uint8_t step;
	void (*process)(void *param);
	
}bleProcessStruct;

在ble.c里建步骤表,然后编写处理过程函数和步骤函数

/*
	查找发送的指令
*/
static int16_t find_at_index_in_table(uint8_t AT)
{
	int16_t index = 0;
	uint8_t table_len = sizeof(ble_at_table) / sizeof(BLEATStruct);
	for(;;)
	{
		if(ble_at_table[index].at_num == AT) break;
		else if(++index > table_len) return -1;
	}
	
	return index;
}




static void ble_process_send_at(void *param);
static void ble_process_wait_recv(void *param);
static void ble_process_recv(void *param);
static void ble_process_fail(void *param);
static void ble_process_end(void *param);

const bleProcessStruct ble_process_table[] = 
{
	{0,		ble_process_send_at	},	/*查表发送数据*/
	{1,		ble_process_wait_recv		},	/*等待信号量或者超时*/
	{2,		ble_process_recv		},	/*成功之后的处理*/
	{3,		ble_process_fail		},	/*失败处理*/
	{4,		ble_process_end	}					/*资源回收*/
};


/*
	步骤0:查找到是哪个AT指令并发送数据
*/
static void ble_process_send_at(void *param)
{
	ble_param.at_index = find_at_index_in_table(ble_param.at_queue->now_at_num);
	memset(ble_data.senddata,0,BLE_SEND_MAX);
	ble_data.sendlen = 0;
	
	/*如果发送回调不为空,则调用*/
	if(ble_at_table[ble_param.at_index].at_send_callback != NULL)
		ble_at_table[ble_param.at_index].at_send_callback(ble_param.at_index,ble_param.at_queue->param);
	
	ble_main.state.bits.suspend = 1;
	HAL_UART_Transmit(&UART_HANDLE,ble_data.senddata,ble_data.sendlen,1000);
	
	ble_main.timer = HAL_GetTick();
	ble_main.step = 1;
	
}

/*
	步骤1:等待回复或者超时
*/
static void ble_process_wait_recv(void *param)
{
	/*如果有回复,则直接跳转到步骤2*/
	if(!ble_main.state.bits.suspend) ble_main.step = 2;
	
	/*等待回复*/
	if(HAL_GetTick() - ble_main.timer > ble_at_table[ble_param.at_index].time_out)
	{
		/*重发*/
		if(++ble_main.resend_cnt >= ble_at_table[ble_param.at_index].resend) ble_main.step = 3;
		ble_main.timer = HAL_GetTick();
		HAL_UART_Transmit(&UART_HANDLE,ble_data.senddata,ble_data.sendlen,1000);
	}
}

/*
	步骤2:收到回复之后的处理
*/
static void ble_process_recv(void *param)
{
	/*对比字符串*/
	if(strstr((char *)ble_data.recvdata,ble_at_table[ble_param.at_index].recv_str) == NULL)
	{
		ble_main.step = 1;
		return ;
	}	
	
	if(ble_at_table[ble_param.at_index].at_recv_callback != NULL)
		ble_at_table[ble_param.at_index].at_recv_callback(ble_param.at_index,param);
	
	ble_main.step = 4;
}

/*
	步骤3:失败的处理
*/
static void ble_process_fail(void *param)
{
	printf("超时\r\n");
	ble_main.step = 4;
}

/*
	步骤4,资源回收
*/
static void ble_process_end(void *param)
{
	/*删除结点*/
	remove_msg_queue_first_node();
	
	/*清除发送数组*/
	memset(ble_data.recvdata,0,BLE_RECV_MAX);
	ble_data.recvlen = 0;
	
	ble_main.resend_cnt = 0;
	ble_main.step = 0;
	
	/*如果消息队列没有消息了,则退出*/
	if(ble_param.at_queue == NULL) ble_main.state.bits.running = 0;

}


/*
	指令处理过程
*/
void ble_at_process(void)
{
	uint8_t step_index = 0;
	uint8_t ble_process_table_len = sizeof(ble_process_table) / sizeof(bleProcessStruct);
	
	if(!ble_main.state.bits.running) return ;	/*如果没有运行标志,直接退出*/
	
	for(;;)
	{
		if(ble_main.step == ble_process_table[step_index].step) break;
		else if(++step_index > ble_process_table_len) return ;
	}
	
	if(ble_process_table[step_index].process != NULL)
		ble_process_table[step_index].process(&ble_param);
	
}

其中ble_at_process()就放在main()里的while(1)运行

那么在哪里释放信号量呢,信号量就说明是模块有返回字符,一般这种AT指令模块都是串口通信,所以我们在接收到一帧数据的时候释放信号量,因为有些模块,他是分多次回复这个AT指令的数据,所以我们希望有自己的缓冲区来持续存储返回的数据,
在ble.h创建接收缓冲区和发送缓冲区结构体

#define  BLE_SEND_MAX   128  /*发送数据最大长度*/
#define  BLE_RECV_MAX	256	  /*接收数据最大长度*/
/*
	存放发送和接收数据的结构体
*/
typedef struct
{
	uint8_t senddata[BLE_SEND_MAX];
	uint8_t recvdata[BLE_RECV_MAX];
	uint16_t sendlen;
	uint16_t recvlen;
}BLEDataStruct;

在ble.c里写复制到缓冲区的函数和释放信号量的函数

/*
	将需要发送的数据加入到发送数组
*/
static void add_data_to_send_buf(uint8_t *data,uint16_t data_len)
{
	if(ble_data.sendlen + data_len < BLE_SEND_MAX)
	{
		memcpy(ble_data.senddata + ble_data.sendlen,data,data_len);
		ble_data.sendlen += data_len;
	}
	
}

/*
	复制数据到接收缓冲区
*/
static void  add_data_to_bc20_recvbuf(uint8_t *data,uint16_t len)
{
	if(ble_data.recvlen + len< BLE_RECV_MAX)
	{
		memcpy(ble_data.recvdata + ble_data.recvlen,data,len);
		ble_data.recvlen += len;
	}
}


/*
	获取ble回复的信息并打印
	接收到蓝牙发送的数据有两种情况,一种的AT的信息,一种是透传的信息
*/
void printf_at_recv(void)
{
	uint16_t recv_len = 0;
	uint8_t *recv = get_uart_recv_data(&UART_HANDLE,&recv_len);
	printf("%s",recv);
	
	/*如果没有连接则,复制到自己的缓冲区*/
	if(BLE_UNLINK_STATE && ble_main.state.bits.running)
	{
		add_data_to_bc20_recvbuf(recv,recv_len);
		ble_main.state.bits.suspend = 0;/*释放信号量,到主线程进行处理*/
	}
	clear_uart_buf(&UART_HANDLE,0);
}


/*
	蓝牙模块初始化
*/
void  ble_init(void)
{
	add_to_uartx_it_callback(&UART_HANDLE,printf_at_recv);
	ble_init_config(1000,"xm-ble");
}

其中ble_init()是在程序开始运行时初始化调用,把释放回调加到串口接收完成的中断里,这是串口模块提供的函数接口,这样做的好处就是模块化,如果AT指令的模块不是串口的接口,那么就可以调用其他模块的函数接口。

3.写给其他程序调用的接口函数

终于到最后一步了,过程都写好之后,就需要提供接口给其他模块调用,比如说需要配置蓝牙模块的名字,广播时间,是否进入低功耗,这个时候就需要输入参数,然后在指令表中的发送回调里将这些参数加入到发送的缓冲区里,但是又有一个问题,这些参数保存在哪里呢,要是我想要发送多条指令呢,这个就需要用到消息队列,把参数(消息)保存在消息队列里,处理完之后在删除消息,具体为:
在ble.h里创建消息队列结构体

/*结点结构体*/
typedef struct _list_node
{
	uint8_t now_at_num;	/*指令编号*/
	void *param;		/*参数*/
	struct _list_node *next;
}ble_at_nodeStruct;	/*at结点*/

/*当前指令编号*/
typedef struct
{
	int16_t at_index;	/*当前at在表里的步长*/
	ble_at_nodeStruct *at_queue;	/*at指令队列*/
	
}BLEParamStruct;

在ble.c里写几个函数,创建新的消息、将消息加入消息队列、删除消息

/*
	创建at指令结点,输入AT指令和参数,参数长度,并存到结点里
*/
static ble_at_nodeStruct *new_msg_queue_node(uint8_t at,void *param_in, uint16_t param_len)
{
	/*为结点申请空间*/
	ble_at_nodeStruct *node = (ble_at_nodeStruct *)malloc(sizeof(ble_at_nodeStruct));
	
	if(node == NULL) return NULL;
	
	node->next = NULL;
	node->now_at_num = at;
	
	if(param_in == NULL) return node;/*如果没有参数*/
	
	/*为数据部分申请空间*/
	uint8_t *param = (uint8_t *)malloc(sizeof(uint8_t) *param_len);
	
	if(param == NULL)
	{
		free(node);node = NULL;
		return NULL;
	}
	
	/*复制参数部分*/
	node->param = param;
	memcpy(node->param,param_in,param_len);
	
	return node;

}

/*
	2.23 加入到消息队列的最后一个结点
*/
static void add_to_msg_queue_tail(ble_at_nodeStruct *node)
{
	ble_at_nodeStruct **tail_node = &ble_param.at_queue;
	ble_at_nodeStruct *next_node = *tail_node;
	
	if(*tail_node == NULL)
	{
		*tail_node = node;
	}else{
		while(next_node->next != NULL)
		{
			next_node = next_node->next;
		}
		next_node->next = node;
	}
	
}

/*
	2.23 删除消息队列中的第一个结点和结点中的消息
*/
static void remove_msg_queue_first_node(void)
{
	ble_at_nodeStruct *first_node = ble_param.at_queue;
	
	if(first_node == NULL) return ;
	
	if(first_node->param != NULL)
	{
		free(first_node->param); first_node->param = NULL;/*释放消息的内存*/
	}
	
	ble_param.at_queue = ble_param.at_queue->next;
	
	free(first_node); first_node = NULL;	/*释放结点内存*/
	
}

然后写AT指令统一入口函数,就是在这里输入参数的地址和参数的长度,用来申请对应大小的内存来存储参数(消息),并且释放任务开始的信号

/*
	所有AT指令入口函数
	参数:at,编号,param:对应的at的参数,param_len:参数字节数
*/
static void ble_at_handler(uint8_t at,void *param,uint16_t param_len)
{
	ble_at_nodeStruct *node = new_msg_queue_node(at,param,param_len);
	
	if(node == NULL) return ;
	
	add_to_msg_queue_tail(node);
	
	ble_main.state.bits.running = 1;
}

最后就是让其他模块调用的接口,就是具体要实现的功能了,比如说配置模块的名字和时间间隔

/*
	对外的接口,配置ble
*/
void ble_init_config(uint16_t broadcast_time,char *name)
{
	
	ble_at_handler(0,NULL,0);
	ble_at_handler(1,&broadcast_time,sizeof(broadcast_time));
	ble_at_handler(2,name,strlen(name));
	
}

这样其他函数只要调用就可以了

结束

整个过程实现下来还是挺麻烦的,而且这只适用于没有操作系统的,用上操作系统的话就简单多了,后面在做一个加上操作系统的。整个过程还是有比较多能够优化的地方,比如说当指令多了之后,查找指令的方法可以改为二分查找,在处理过程中代码格式的优化,希望各位大佬能够多多指出。