FireBLE 低功耗智能开源平台 立即购买

FireBLE 是一个面向于打造智能生活的开源平台,以BLE(Bluetooth Low Energy)技术为核心,拥有超低的功耗、不俗的处理能力和广泛的应用场合,专注于更智能、高效率的工作模式,让生活在科技中更安全、方便、快捷。也许您一个不经意的想法与FireBLE擦出的火花,会在这片原野上燎出火焰,甚至燃烧整个世界。

串口透传

更新时间:2017-08-08 阅读:2379

前言

本项目为技术案例中串口透传的源码解析,希望关注FireBLE的朋友都能够在本篇文章中获得启发。

串口透传本身还未完善,许多冗余处和缺陷、排版都有待整理,希望大家不要见怪。

首先介绍串口透传固件的功能(有一些技术案例未展示出来):

  • 在140个字符内任意长度数据透传。
  • 串口波特率可配置,默认9600,8位传输,1位停止位,无校验,无流控。
  • 支持串口AT命令,通过IO口切换AT模式以及传输模式。AT命令可实现一些基本配置(获取版本、设备名、设备地址。。)、基本操作(开启广播、关闭广播、断开连接。。。)以及一些基本驱动命令(PWM、GPIO、I2C、SPI)。
  • 配套APP(Android以及IOS),支持APP端AT命令。
  • 固件空中升级。

防丢器中最主要是采用了proxr的协议来实现,防丢器包含以下功能

  • 开启/关闭广播
  • 断开后自动广播
  • 报警
  • 反向报警

基础例程

拷贝一个qpps的例程过来,做基础例程,拷贝过来后可以做如下操作。

  • 修改文件夹名称、工程文件以及工程配置文件名称
  • 进入例程,修改工程名,修改输出文件名。
  • 先编译一次,产生链接文件

例程修改

串口透传相对于qpps就是增加了串口的功能,将串口接收到的数据透传到主机,将主机透传过来的数据发送到串口。

对于qpps部分,我们做了如下修改:

  • 修改UUID。
  • 修改notify特征数为1。

对于串口部分:

  • 保留串口0作为串口调试信息打印,用串口1作为透传串口。可在usr_config.h中配置和转换。
  • 添加串口1的接收、发送。
  • 增加串口AT命令。

对于fireble部分:

  • 一个服务,包含一个可写属性特征、可notify属性特征。
  • 能接收AT命令并且处理,返回处理结果。

添加OTA升级

QPPS修改

详细修改请查看源码,本文最后会对这些修改用到的概念进行一些分析。

串口功能添加

串口功能要实现收和发,可以先从收入手。在例程client中,有进行串口接收输入命令并且打印命令反馈的例程,可供参考。

首先对串口1初始化

将串口1中断打开

将串口1中断打开

#define CONFIG_ENABLE_DRIVER_UART1                      TRUE        /*!< Enable/Disable UART Driver */
#define CONFIG_UART1_TX_DEFAULT_IRQHANDLER              TRUE       /*!< Enable/Disable UART1 TX Default IRQ Handler */
#define CONFIG_UART1_RX_DEFAULT_IRQHANDLER              TRUE       /*!< Enable/Disable UART1 RX Default IRQ Handler */
#define CONFIG_UART1_TX_ENABLE_INTERRUPT                TRUE       /*!< Enable/Disable(Polling) UART1 TX Interrupt */
#define CONFIG_UART1_RX_ENABLE_INTERRUPT                TRUE       /*!< Enable/Disable(Polling) UART1 RX Interrupt */

不使用ROM中的UART驱动

#define CONFIG_ENABLE_ROM_DRIVER_UART                   FALSE       /*!< Enable/Disable UART ROM Driver */

开启串口中断唤醒

#define UART_RX_ACTIVE_BIT_EN                           TRUE       /*!< Enable/Disable uart rx active bit set */

配置串口GPIO引脚复用功能

对于GPIO的引脚复用,可能很多朋友都不习惯用QBlue中的QBueDriverTool这个工具去自动配置,喜欢直接修改GPIO,在配置串口1的时候,如果例程选用不当,那么就会出问题,以下第二段如果经过工具配置是将0赋值到PMCR2寄存器,如果采用qpps的自带例程的配置,串口1的功能使用将会出现问题,因为这个引脚也是配置引脚复用功能的。

#if (!QN_COM)
                    | P10_GPIO_8_PIN_CTRL
                    | P11_GPIO_9_PIN_CTRL
#else
                    | P10_UART1_RXD_PIN_CTRL
                    | P11_UART1_TXD_PIN_CTRL
#endif
    // pin select
    syscon_SetPMCR2(QN_SYSCON, 0);

串口驱动初始化

串口初始化函数和中断开启,这里一共配置了2次,一次在system.c的SystemInit中,一次在usr_sleep_restore中。usr_sleep_restore中需要重新配置的原因是sleep后外设时钟都会停止掉,需要再启动一遍,中断也需要重新开启。

#if defined(QN_COM_UART)
    // Initialize User UART port
    uart_init(QN_COM_UART, USARTx_CLK(0), UART_9600);
    uart_tx_enable(QN_COM_UART, MASK_ENABLE);
    uart_rx_enable(QN_COM_UART, MASK_ENABLE);
#endif

com_env

串口1的全局变量较多,我们把这些公用的属性整合进com_env中方便管理。

struct com_env_tag
{
		uint8_t com_conn;
    uint8_t com_mode;
    ///Message id
    uint8_t msg_id;
 
    ///UART TX parameter 
    uint8_t tx_state;       //either transmitting or done.
    struct co_list queue_tx;///Queue of kernel messages corresponding to packets sent through HCI
    struct co_list queue_rx;///Queue of kernel messages corresponding to packets sent through HCI
 
    ///UART RX parameter 
    uint8_t com_rx_len;
		uint8_t com_at_len;
    uint8_t com_rx_buf[QPPS_VAL_CHAR_NUM_MAX*QPP_DATA_MAX_LEN];
    uint8_t com_at_buf[COM_AT_COMM_BUF];
		bool		auto_line_feed;
 
};

串口app的初始化,初始化了三个事件,分别为发送完毕、接收帧满和接收超时。

void com_init(void)
{
    //for com uart tx
    com_env.tx_state = COM_UART_TX_IDLE;	//initialize tx state
    com_env.com_conn = COM_DISCONN;
    com_env.com_mode = COM_MODE_IDLE;
    com_env.auto_line_feed = COM_NO_LF;
    co_list_init(&com_env.queue_tx);			//init TX queue
    co_list_init(&com_env.queue_rx);			//init RX queue
 
    com_gpio_init();
 
		show_com_mode(com_env.com_mode);
 
    if(KE_EVENT_OK != ke_evt_callback_set(EVENT_UART_TX_ID, com_tx_done))
        ASSERT_ERR(0);
    if(KE_EVENT_OK != ke_evt_callback_set(EVENT_UART_RX_FRAME_ID, com_event_uart_rx_frame_handler))
        ASSERT_ERR(0);
    if(KE_EVENT_OK != ke_evt_callback_set(EVENT_UART_RX_TIMEOUT_ID, com_event_uart_rx_timeout_handler))
        ASSERT_ERR(0);
 
}

这是AT部分的初始化,使用了GPIO来产生中断切换AT模式与传输模式,还设置了两个事件。

void com_gpio_init(void)
{
    //set wakeup config,when GPIO low and trigger interrupt
    gpio_wakeup_config(COM_AT_ENABLE,GPIO_WKUP_BY_LOW);
    gpio_enable_interrupt(COM_AT_ENABLE);
 
 
    if(KE_EVENT_OK != ke_evt_callback_set(EVENT_AT_ENABLE_PRESS_ID,
                                          app_event_at_enable_press_handler))
    {
        ASSERT_ERR(0);
    }
 
    if(KE_EVENT_OK != ke_evt_callback_set(EVENT_AT_COMMAND_PROC_ID,
                                          app_com_at_command_handler))
    {
        ASSERT_ERR(0);
    }
 
}

串口接收

串口接收首先清楚当前寄存器内容和中断标志位,防止错误输入。然后开始读取,设置回调函数。每次接收一个字符。

void	com_uart_rx_start(void)
{
    uart_uart_ClrIntFlag(CFG_COM_UART,0x1ff);
    uart_uart_GetRXD(CFG_COM_UART);
		com_env.com_rx_len = 0;
		uart_read(QN_COM_UART, &com_env.com_rx_buf[com_env.com_rx_len],1, com_uart_rx);
}

串口接收数据满了,就发出帧满的事件;如果接收未满,则读取下一个字符,并且设定超时事件。

void com_uart_rx(void)
{
    //continue receive the data for RX
    com_env.com_rx_len++;
    //set pt gpio state
    if(com_env.com_rx_len == QPPS_VAL_CHAR_NUM_MAX*QPP_DATA_MAX_LEN)  //receive data buf is full, should sent them to ble
    {
        ke_evt_set(1UL << EVENT_UART_RX_FRAME_ID);
    }
    else
    {
        uart_read(QN_COM_UART, &com_env.com_rx_buf[com_env.com_rx_len], 1, com_uart_rx);
        ke_evt_set(1UL << EVENT_UART_RX_TIMEOUT_ID);
    }
}

如果接收数据满了,就停止接收,关闭接收中断,发送APP_COM_UART_RX_DONE_IND消息,然后将接收到的数据作为消息参数传递过去。记得要清除掉定时器和事件。

void com_event_uart_rx_frame_handler(void)
{
    uart_rx_int_enable(QN_COM_UART, MASK_DISABLE);  //disable uart rx interrupt
    struct app_uart_data_ind *com_data = ke_msg_alloc(APP_COM_UART_RX_DONE_IND,
                                         TASK_APP,
                                         TASK_APP,
                                         com_env.com_rx_len+1);
    com_data->len=com_env.com_rx_len;
    memcpy(com_data->data,com_env.com_rx_buf,com_env.com_rx_len);
    ke_msg_send(com_data);
 
    ke_timer_clear(APP_COM_RX_TIMEOUT_TIMER, TASK_APP);
    ke_evt_clear(1UL << EVENT_UART_RX_FRAME_ID);
}

设定一个定时器,当定时器发送时间为到来时下一个数据到达,则会覆盖掉当前的计时,重新开始下一轮定时器计时。如果到时间了下一个数据还没有到来,那么判断数据接收完成,发起定时器中断事件。

void com_event_uart_rx_timeout_handler(void)
{
    ke_timer_set(APP_COM_RX_TIMEOUT_TIMER, TASK_APP, COM_FRAME_TIMEOUT);
    ke_evt_clear(1UL << EVENT_UART_RX_TIMEOUT_ID);
}

定时器中断事件同样向APP_COM_UART_RX_DONE_IND发出了消息,并且把接收到的数据作为参数传递过去。

int app_com_rx_timeout_handler(ke_msg_id_t const msgid, void const *param,
                               ke_task_id_t const dest_id, ke_task_id_t const src_id)
{
    uart_rx_int_enable(QN_COM_UART, MASK_DISABLE);  //disable uart rx interrupt
    struct app_uart_data_ind *com_data = ke_msg_alloc(APP_COM_UART_RX_DONE_IND,
                                         TASK_APP,
                                         TASK_APP,
                                         com_env.com_rx_len+1);
    com_data->len=com_env.com_rx_len;
    memcpy(com_data->data,com_env.com_rx_buf,com_env.com_rx_len);
    ke_msg_send(com_data);
 
    return (KE_MSG_CONSUMED);
}

由于数据长度大于20个字节,所以需要分包发送。分包发送主要使用了内核中的列表来作为缓存区存放数据,如果数据小于20字节,就直接将数据转换为消息参数,存入列表中,否则就以20字节为一包做分包存入。

void dev_send_to_app(struct app_uart_data_ind *param)
{
    uint8_t *buf_20;
    int16_t len = param->len;
    int16_t send_len = 0;
 
		uint8_t packet_len = get_bit_num(app_qpps_env->char_status)*20;
#ifdef	CATCH_LOG
	QPRINTF("\r\ntx len %d  data : ",len);
 
    for(uint8_t j = 0; j<len; j++)
        QPRINTF("%c",param->data[j]);
    QPRINTF("\r\n");
#endif
    if(app_qpps_env->char_status)
    {
        for(uint8_t i =0; send_len < len; i++)
        {
            if (len > packet_len) //Split data into package when len longger than 20
            {
                if (len - send_len > packet_len)	
                {
                    buf_20 = (uint8_t*)ke_msg_alloc(0, 0, 0, packet_len);
                    if(buf_20 != NULL)
                    {
                        memcpy(buf_20,param->data+send_len,packet_len);
                        send_len+=packet_len;
                    }
                }
                else
                {
                    buf_20 = (uint8_t *)ke_msg_alloc(0,0,0,len-send_len);
                    if (buf_20 != NULL)
                    {
                        memcpy(buf_20,param->data+send_len,len-send_len);
                        send_len = len;
                    }
                }
            }
            else	//not longger ther 20 send data directely
            {
								buf_20 = (uint8_t *)ke_msg_alloc(0,0,0,len);
								if (buf_20 != NULL)
								{
										memcpy(buf_20,param->data,len);
										send_len = len;
								}
                //app_qpps_data_send(app_qpps_env->conhdl,0,len,param->data);
            }
						//push the package to kernel queue.
						app_push(ke_param2msg(buf_20));
				}
    }
}

一个特征值一次最多传输 20 个字节,如果在消息未确认发送完成的时候就继续填入消息,那么之前的消息会被覆盖,所以在第一次存入数据时,需检查app_qpps_env->char_status是否有空闲特征可以用于发送数据,如果有则发送出去,同时将app_qpps_env->char_status清 0 ,等到收到发送成功消息QPPS_DATA_SEND_CFM后,再将app_qpps_env->char_status置 1 ,防止发生消息覆盖。

void app_push(struct ke_msg *msg)
{
    // Push the message into the list of messages pending for transmission
    co_list_push_back(&com_env.queue_rx, &msg->hdr);
    //QPRINTF("\r\n@@@app_push:");
//    for (uint8_t i = 0; i<msg->param_len; i++)
//        QPRINTF("%c",((uint8_t *)&msg->param)[i]);
//    QPRINTF("\r\n");
 
		//only send in the first push.
		uint8_t *p_data = (uint8_t *)msg->param;
		uint8_t pack_nb = msg->param_len/QPP_DATA_MAX_LEN + 1;
		uint8_t pack_divide_len =  msg->param_len%QPP_DATA_MAX_LEN;
		for (uint8_t char_idx = 0,i = 0;((app_qpps_env->char_status & (~(QPPS_VALUE_NTF_CFG << (char_idx - 1) ))) && (char_idx < QPPS_VAL_CHAR_NUM ));char_idx++)
		{
			if (i < (pack_nb - 1))
			{
				app_qpps_env->char_status &= ~(QPPS_VALUE_NTF_CFG << char_idx);
				app_qpps_data_send(app_qpps_env->conhdl,char_idx,QPP_DATA_MAX_LEN,(uint8_t *)p_data);
				p_data += QPP_DATA_MAX_LEN;
			}
			else
			{
				if ((pack_divide_len != 0) && (i == (pack_nb - 1)))
				{
					app_qpps_env->char_status &= ~(QPPS_VALUE_NTF_CFG << char_idx);
					app_qpps_data_send(app_qpps_env->conhdl,char_idx,pack_divide_len,(uint8_t *)p_data);
					p_data += pack_divide_len;
				}
			}
			i++;
		}
}

接收到QPPS_DATA_SEND_CFM消息后,重置推送特征空闲状态的标志位,如果全部特征发送完毕,那么判断列表是否为空,如果为空则继续取包发送,此时取包发送时调用的是app_tx_done函数。

	case QPPS_DATA_SEND_CFM:
	{
		app_qpps_env->char_status |= (QPPS_VALUE_NTF_CFG << ((struct qpps_data_send_cfm *)param)->char_index);
#if QN_COM							
		uint8_t bit_num = get_bit_num(app_qpps_env->char_status);
		/// if com_mode is  COM_MODE_TRAN and data has send to client success,continiu receive data from com
		if (bit_num >= QPPS_VAL_CHAR_NUM)
		{										
			if (!co_list_is_empty(&com_env.queue_rx))
				app_tx_done();
			if(com_env.com_mode == COM_MODE_TRAN)
			{
				com_uart_rx_start();
			}
		}
#endif								
	}
	break;
void app_tx_done(void)
{
    struct ke_msg * msg;
    //release current message (which was just sent)
    msg = (struct ke_msg *)co_list_pop_front(&com_env.queue_rx);
    // Free the kernel message space
    ke_msg_free(msg);
    // Check if there is a new message pending for transmission
    if ((msg = (struct ke_msg *)co_list_pick(&com_env.queue_rx)) != NULL)
    {
        // Forward the message to the HCI UART for immediate transmission
//        QPRINTF("\r\napp_tx_done:");
//        for (uint8_t i = 0; i<msg->param_len; i++)
//            QPRINTF("%c",((uint8_t *)&msg->param)[i]);
//        QPRINTF("\r\n");
 
 
				uint8_t *p_data = (uint8_t *)msg->param;
				uint8_t pack_nb