市场上有很多的模块,比如蓝牙,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指令的处理过程了,大概的流程就是
这个处理过程主要就是把整个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));
}
这样其他函数只要调用就可以了
结束
整个过程实现下来还是挺麻烦的,而且这只适用于没有操作系统的,用上操作系统的话就简单多了,后面在做一个加上操作系统的。整个过程还是有比较多能够优化的地方,比如说当指令多了之后,查找指令的方法可以改为二分查找,在处理过程中代码格式的优化,希望各位大佬能够多多指出。