串口透传
更新时间:2017-08-08 阅读:5035
目录
前言
本项目为技术案例中串口透传的源码解析,希望关注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