打印
[MM32硬件]

使用MindSDK的at-server组件开发从机模组

[复制链接]
775|3
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
tpgf|  楼主 | 2024-7-15 08:17 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
引言
多年之前使用SIM800C模组时接触了AT命令,后来自己设计主从机通信应用场景时,需要以一种最简单方便的方式实现基于UART的控制通信协议,以实现人机交互,就一直想自行实现一套AT命令的主机框架。也找了一些开源的项目,但多少有些不尽如意。在MindSDK的开发过程中,借鉴了RT-Thread对AT组件的实现方式,实现了一套简易的AT Server的框架。AT Server特别适合小资源微控制器,做一些功能模块的接口,或者实现PC机和微控制器的人机交互。

AT命令应用场景
AT指令(Attention Commands)最早是由发明拨号调制解调器(MODEM)的贺氏公司(Hayes)为了控制MODEM而发明的控制协议。随着网络带宽的升级,速度很低的拨号MODEM基本退出了一般的使用市场,但是AT指令保留了下来。当时主要的移动电话生产厂家共同为GSM研制了一整套AT指令,用于控制手机的GSM模块。AT 指令在此基础上演化并加入GSM 07.05标准以及后来的GSM 07.07标准,实现了比较健全的标准化。

在随后的GPRS控制、3G模块等方面,均采用AT指令来控制,AT指令逐渐在产品开发中成为实际的标准。如今,AT指令也广泛应用于嵌入式开发领域,AT指令作为主芯片和通讯模块的协议接口,硬件接口一般为串口,这样主控设备可以通过简单的指令和硬件设计完成多种操作。

在嵌入式开发中,经常是使用AT命令去控制各种通讯模块,比如ESP8266 WIFI模块、4G模块、GPRS模块等等。一般就是主芯片通过硬件接口(比如串口、SPI)发送AT命令给通讯模块,模块接收到数据之后回应响应的数据。

AT命令技术简介
AT命令通信系统由AT客户端(Client)和AT服务器(Server)组成,如图x所示。



图x AT通信系统中的客户端和服务器
AT客户端通常是主控芯片,AT服务器通常是各种通讯模组。

AT命令由三个部分组成:

前缀。AT两个字符。
主体。命令和参数。
结束符。一般为 <CR><LF> (“\r\n”)。
例:AT+CWMODE=3\r\n
AT Server 返回给 AT Client 的数据有两种:

命令响应数据。回应响应的状态和信息
URC 数据(unsolicited result code)。AT Server 主动发送给 AT Client 的数据。
例1: AT Server接收到网络的数据后,会主动把这些数据发送给 AT Client。
例2: WIFI 断开连接等,会主动发数据告知 AT Client。
MindSDK中的at-server组件及样例工程
MindSDK(https://mindsdk.mindmotion.com.cn/)是灵动微电子官方发布的适用于灵动微控制器的软件开发和发布平台,其中的at-server组件实现了AT Server的功能,可对AT命令的进行解析,并使用开发者注册到对应命令格式上的函数执行相关操作。开发者可以在MindSDK在线平台上下载任意板子上包含AT组件的样例工程(例如FTHR-F0140板上的at_basic),从而获得MindSDK的at-server组件的源码。

开发者在使用at-server组件时,需要为每个命令的不用子命令注册自定义的回调函数。每个AT命令<x>可以对应4个子命令:



例如:可设计“LED”命令,绑定对电路板上一个小灯的控制

AT+LED 初始化LED灯。
AT+LED?查看当前LED灯状态。
AT+LED=1或者AT+LED=0 设置LED状态为亮或者暗。
MindSDK中为几乎所有的开发板适配了at_basic样例工程,这个工程包含了at_server组件,并展示了在实现的AT Server中添加一系列控制LED小灯的命令。

以FTHR-F0140开发板为例,其at_basic工程的组织结构,如图x所示。



图x at-server工程组织
除了MindSDK中提供的at_server组件的源码文件at_server.h和at_server.c文件,还有一些适配源码。

at_port.c
at_port.c文件中的源代码将at-server绑定到具体平台的硬件上,在本例中,使用MM32F0140微控制器的UART2作为传输AT命令的端口。其中,就是要实例化一个at_adapter_t类型的结构体对象at_adapter,并向其中注册操作硬件通信引擎的函数字段。

/*
* Macros.
*/
#define BOARD_AT_BUF_SIZE   64u  /* the at server receive buffer size. */

/*
* Variables.
*/

static char     at_adapter_buf[BOARD_AT_BUF_SIZE]; /* the buffer for at server. */

/*
* Declerations.
*/

static void     at_init(void);
static uint32_t at_read(char * buf, uint32_t len);
static void     at_write(char * buf, uint32_t len);

/* initialize the at adapter. */
const at_adapter_t at_adapter =
{
    .init       = at_init, /* initialize the uart hardware. */
    .read       = at_read, /* read charactors from uart. */
    .write      = at_write, /* write charactors into uart. */
    .buf        = at_adapter_buf,
    .buf_size   = BOARD_AT_BUF_SIZE,
};



此处,.buf指定的缓冲区,将存放从UART总线上捕获到的字符串,at-server解析其中的内容,识别命令,并调用开发者注册到对应子命令的回调函数。

初始化函数.init和发送函数.write均绑定到硬件的UART端口。

/* init func. */
static void at_init(void)
{
    /* init rbuf. */
    rbuf_init(&rbuf_handler, rbuf_buf, sizeof(rbuf_buf) );
    /* init at uart port. */
    UART_Init_Type uart_init;
    uart_init.ClockFreqHz   = BOARD_AT_UART_FREQ;
    uart_init.BaudRate      = BOARD_AT_UART_BAUDRATE;
    uart_init.WordLength    = UART_WordLength_8b;
    uart_init.StopBits      = UART_StopBits_1;
    uart_init.Parity        = UART_Parity_None;
    uart_init.XferMode      = UART_XferMode_RxTx;
    uart_init.HwFlowControl = UART_HwFlowControl_None;
    uart_init.XferSignal    = UART_XferSignal_Normal;
    uart_init.EnableSwapTxRxXferSignal = false;
    UART_Init(BOARD_AT_UART_PORT, &uart_init);
    UART_Enable(BOARD_AT_UART_PORT, true);
    /* enable rx interrupt. */
    UART_EnableInterrupts(BOARD_AT_UART_PORT, UART_INT_RX_DONE, true);
    NVIC_EnableIRQ(BOARD_AT_UART_IRQn);
}

/* write data by uart. */
static void at_write(char * buf, uint32_t len)
{
    for (uint32_t i = 0u; i < len; i++)
    {
        while ( 0u == (UART_STATUS_TX_EMPTY & UART_GetStatus(BOARD_AT_UART_PORT)) )
        {}
        UART_PutData(BOARD_AT_UART_PORT,buf);
    }
}



在本例中,使用中断方式实现从UART接收数据的过程,,将at_read()函数注册到at_adapter的.read字段中。这里借用了MindSDK中rbuf组件实现FIFO队列,建立UART接收中断服务程序和接口函数之间的数据管道。

#include "rbuf.h"

static uint8_t  rbuf_buf[BOARD_AT_BUF_SIZE]; /* the buffer for rbuf. */
static rbuf_handler_t rbuf_handler; /* rbuf handler. */

/* read data from rbuf. */
static uint32_t at_read(char * buf, uint32_t len)
{
    uint32_t read_cnt = 0u;
    for (; read_cnt < len; read_cnt++)
    {
        if (rbuf_is_empty(&rbuf_handler) )
        {
            break;
        }
        buf[read_cnt] = rbuf_output(&rbuf_handler);
    }
    return read_cnt;
}

/* the interrupt handler of uart. */
void BOARD_AT_UART_IRQHandler(void)
{
    uint32_t flag = UART_GetInterruptStatus(BOARD_AT_UART_PORT);

    if ( 0u!= (UART_INT_RX_DONE & flag) )
    {
        /* get byte. */
        uint8_t data = UART_GetData(BOARD_AT_UART_PORT);

        /* in queue. */
        rbuf_input(&rbuf_handler, data);
    }
    UART_ClearInterruptStatus(BOARD_AT_UART_PORT, flag);
}



at_cmd_led.c
在at_cmd_led.c文件中,创建了AT+LED命令组,实例化一个at_cmd_t类型的对象at_cmd_led,以及其中的函数字段。其中的函数字段就对应了该命令的不同子命令的具体执行内容。

/* to query the parameter format and value range for AT+LED command. */
at_result_t at_cmd_led_test(at_server_write_t write);

/* to return command parameters. */
at_result_t at_cmd_led_query(at_server_write_t write);

/* to set user-specified parameters into the corresponding function. */
at_result_t at_cmd_led_setup(at_server_write_t write, char * args);

/* to perform related operations. */
at_result_t at_cmd_led_exec(at_server_write_t write);

const at_cmd_t at_cmd_led =
{
    .name = "AT+LED",
    .args_expr = "<value>",
    .test  = at_cmd_led_test,  /* AT+LED=?    */
    .query = at_cmd_led_query, /* AT+LED?     */
    .setup = at_cmd_led_setup, /* AT+LED=1, 0 */
    .exec  = at_cmd_led_exec   /* AT+LED      */
};




其中,注册到.exec字段的at_cmd_led_exec()函数将在本机(AT Server)捕获到AT+LED命令时被调用,本例中,将其实现为初始化控制LED灯对应的GPIO引脚。而注册到.setup字段的at_cmd_led_setup()函数,将在本机捕获到命令字符串AT+LED=0或者AT+LED=1时被调用,at-server组件将根据.args_expr中指定的参数格式解析传入的参数格式,用0或1对应控制小灯的灭和亮的状态。

/* when sending AT+LED=<value>, the value can be 0 or 1 or other, will call this function. */
at_result_t at_cmd_led_setup(at_server_write_t write, char * args)
{
    char buf[AT_CMD_BUF_SIZE];
    uint32_t param; /* the command parameter. */

    if (1u == at_req_parse_args(args, "=%d", &param))
    {
        if (param == 1)
        {
            GPIO_WriteBit(BOARD_LED0_GPIO_PORT, BOARD_LED0_GPIO_PIN, 1u);
        }
        else if (param == 0)
        {
            GPIO_WriteBit(BOARD_LED0_GPIO_PORT, BOARD_LED0_GPIO_PIN, 0u);
        }
    }
    else
    {
        sprintf(buf, "ERROR \r\n");
    }
    write(buf, strlen(buf));

    return AT_RESULT_OK;
}

/* when sending AT+LED command will call this function. to exectue led initialization. */
at_result_t at_cmd_led_exec(at_server_write_t write)
{
    char buf[AT_CMD_BUF_SIZE];

    /* LED initialize. */
    GPIO_Init_Type gpio_init;
    gpio_init.Pins  = BOARD_LED0_GPIO_PIN;
    gpio_init.PinMode  = GPIO_PinMode_Out_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(BOARD_LED0_GPIO_PORT, &gpio_init);
    GPIO_WriteBit(BOARD_LED0_GPIO_PORT, BOARD_LED0_GPIO_PIN, 1u);
    sprintf(buf, "OK\r\n");
    write(buf, strlen(buf));

    return AT_RESULT_OK;
}



main.c
在at_port.c文件中将at-server组件绑定到MM32F0140硬件平台、在at_cmd_led.c文件中创建了一组自定义的AT+LED命令后,最后需要在main.c文件中为运行at-server组件创建执行线程,调用at-server的服务。

在本例工程中的main.c文件中,有代码如下:

#include "board_init.h"
#include "at_server.h"

/*
* Declerations.
*/
extern const at_cmd_t at_cmd_at;
extern const at_cmd_t at_cmd_led;
/*
* Variables.
*/
/* set the at command tables. */
const at_cmd_t * at_cmd_list[]=
{
    &at_cmd_at,
    &at_cmd_led
};
extern const at_adapter_t at_adapter;
at_server_t at_server_local;

/*
* Functions.
*/
int main(void)
{
    BOARD_Init();

    /* at server initialization. */
    at_server_init(&at_server_local,(at_adapter_t *)&at_adapter);
    /* set the at command list for at server. */
    at_server_set_cmd_list( &at_server_local, (at_cmd_t **)at_cmd_list, sizeof(at_cmd_list)/sizeof(* at_cmd_list));

    while (1)
    {
        at_server_task(&at_server_local); /* loop the at server task. */
    }
}

/* EOF. */



在main()函数中:

首先调用at_server_init()函数,初始化一个AT Server,并传入at_adapter绑定硬件平台。
调用at_server_set_cmd_list()函数中,向AT Server传入命令集at_cmd_list。
在while(1)循环中调用at_server_task()函数,解析从硬件通信通道捕获到的包含AT命令的字符串。
至此,编译整个工程,下载可执行文件到芯片中,就可以把AT命令用起来了。

基于AT命令的人机交互应用
实现AT Server的意义在于能够实现主从机的通信,但如果暂时没有微控制器作为主机,也可以直接使用PC机同AT Server小模组进行通信,实现人机交互。

这里演示两种使用PC作为主机的**:使用串口调试软件工具和Python脚本。其中,使用串口调试软件工具可以快速验证功能,而使用Python就更有趣了,可以在PC上运行算法(例如通过USB摄像头捕获图像数据,并使用一些重量级的工具进行图像处理和模式识别),然后连接微控制器的小板子上控制电气系统(例如控制一组舵机完成某些机械动作)。

本例实现的AT Server,使用两种不同的方法,都能实现接收到主机通过UART串口发送的AT+LED命令,控制小灯闪烁。如图x所示。



图x PC机通过AT命令控制FTHR-F0140电路板上的小灯闪烁
使用串口调试助手软件发送AT命令
用户可以下载SSCOM软件(http://www.sscom.vip/),支持预定义的AT命令。运行本例中的工程,如图x所示。



图x 使用SSCOM工具发送AT命令
使用Python脚本发送AT命令
用户可以编写Python脚本,使用PySerial类模块,使用UART串口通信。可编写如下Python脚本,发送AT+LED命令,控制小灯闪烁。

import serial
import time

# connect the UART
ser = serial.Serial()
ser.port='COM9'
ser.baudrate=9600
ser.bytesize=8
ser.stopbits=1
ser.parity='N'
ser.open()

if (ser.isOpen()):
    print('succ')
else:
    print('fail')

# init the led.
ser.write('AT+LED\r\n'.encode('utf-8'))

# control the led.
for i in range(10):
    time.sleep(0.2)
    ser.write('AT+LED=1\r\n'.encode('utf-8'))
    print('led on')

    time.sleep(0.2)
    ser.write('AT+LED=0\r\n'.encode('utf-8'))
    print('led off')

print('done.')

ser.close()




总结
MindSDK中的at-server组件,应用逻辑还是相当清晰的,抽象出了绑定硬件的函数对象、命令集等,自定义命令和注册回调函数也非常方便。at-server对于小资源微控制器来说,可是个福音,在基于主从机交互的应用环境中,将微控制器作为一个仅连接电路系统的控制器,解析并执行来自主机的命令,将大算力和大存储需求的算法和应用逻辑转移到主机(PC机)上。如此以来,不通硬件和不愿意看微控制器开发手册的Python算法工程师,也可以试着让自己的程序控制电路啦。

样例工程 fthr-f0140_at_basic_mdk.zip的下载页面:https://download.csdn.net/download/suyong_yq/87764732
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/suyong_yq/article/details/130563487

使用特权

评论回复
沙发
shenxiaolin| | 2024-7-29 21:11 | 只看该作者
感谢分享

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1923

主题

15596

帖子

11

粉丝