一 需要准备的
① 仔细阅读《SWRU393_CC2640_BLE_Software_Developer's_Guide》官方文档
②《ble_cc26xx_2_01_00_44423》蓝牙协议栈
③ IAR+XDS100V3调试器
④ 功能比较全的开发板
二 目的
在TI-RTOS的框架下,实现LED与BUTTON的交互功能,以此来熟悉TASK的编写方法,方便于后期创建功能更加丰富的应用程序。
LED是设备的简单输出设备,高级形式就是LCD屏幕,或者网络设备输出报文。
BUTTON是简单的输入设备,高级形式可以升级为触摸屏或者网络设备输入报文。
很显然,BUTTON一般是由外部中断来实现的,TI-RTOS严格管理了外部的中断事件源。
datasheet中指明
RTOS根据优先级分了四个层级,HWI>SWI>TASK>IDLE
HWI由硬件事件来触发,且优先级极高,由芯片内部决定最高的优先级。
因此,可知配置外部中断的中断事件是由HWI机制管理的。
TI-RTOS中的消息机制
如果我在app中创建了一个处理按键的TASK,并且认为HWI也是一个TASK并且HWI管理了外部中断源,简单来说,只有HWI的TASK可以知道键有没有被按下,那么如何通知app中处理按键的TASK呢?
datasheet中指明,TI-RTOS采用queue和其他很多机制来实现消息的传递
显然的是 TASKA 与 TASKB共享了一个消息队列,TASKB与TASKA相互独立,也就是TASKA与TASKB的执行顺序互不相干,没有说TASKB一定在TASKA之后执行。
思考一下,在不知道TASKA有没有在我之前执行,不知道其在什么时候执行,但是我作为TASKB需要得知他的工作结果,该如何处理这个问题?
这边给出的方法是等待,TASKB长时间处于BLOCK(阻塞状态)程序不运行,当TASKA收到紧要的消息,如,得知了外部的按键被按下了,这时候会做第一件事,post信号量,唤醒TASKB,然后就是将这个紧要的消息推入与TASKB共享的消息队列,然后TASKA的任务就结束了。TASKB被唤醒,检查队列是否为空,推出消息开始处理!
如datasheet图
总共五个按键硬件连接,见定义
#define Board_KEY_SELECT IOID_11
#define Board_KEY_UP IOID_19
#define Board_KEY_DOWN IOID_12
#define Board_KEY_LEFT IOID_15
#define Board_KEY_RIGHT IOID_18
创建board_key.c
#include <stdbool.h>
#include <ti/sysbios/knl/Clock.h>
#include <ti/sysbios/family/arm/m3/Hwi.h>
#include <ti/sysbios/knl/Semaphore.h>
#include <ti/sysbios/knl/Queue.h>
#include <ti/drivers/pin/PINCC26XX.h>
#include <ICall.h>
#include <inc/hw_ints.h>
#include "bcomdef.h"
#include "PIN.h"
#include "util.h"
#include "board_key.h"
// Value of keys Pressed
static uint8_t keysPressed;
// Key debounce clock
static Clock_Struct keyChangeClock;
// Pointer to application callback
keysPressedCB_t appKeyChangeHandler = NULL;
// Memory for the GPIO module to construct a Hwi
Hwi_Struct callbackHwiKeys;
//call back function
static void Board_keyChangeHandler(UArg a0);
static void Board_keyCallback(PIN_Handle hPin, PIN_Id pinId);
PIN_Config keyPinsCfg[] =
{
Board_KEY_SELECT | PIN_GPIO_OUTPUT_DIS | PIN_INPUT_EN | PIN_PULLUP,
Board_KEY_UP | PIN_GPIO_OUTPUT_DIS | PIN_INPUT_EN | PIN_PULLUP,
Board_KEY_DOWN | PIN_GPIO_OUTPUT_DIS | PIN_INPUT_EN | PIN_PULLUP,
Board_KEY_LEFT | PIN_GPIO_OUTPUT_DIS | PIN_INPUT_EN | PIN_PULLUP,
Board_KEY_RIGHT | PIN_GPIO_OUTPUT_DIS | PIN_INPUT_EN | PIN_PULLUP,
PIN_TERMINATE
};
PIN_State keyPins;
PIN_Handle hKeyPins;
/*
* brief 按键的初始化函数
* PIN_open(&keyPins, keyPinsCfg) 是为了创建PIN列表和PIN操作相关的动态内存空间,返回PIN_Handle型的数据,
* keyPinsCfg是使用到的PIN列表,是一个U32型的数据,格式按规则仿造,第一个参数是一个软件的ID,不与实际的硬件IO口相对应
*
* PIN_registerIntCb(hKeyPins, Board_keyCallback)是用来注册PIN相关HWI事件发生后需要回调的函数指针,见PIN.h的说明
* * Registers a callback function (see #PIN_IntCb for details) for the client
* * identified by handle that will be called from HWI context upon an interrupt
* * event on one or more of the allocated pins that have interrupts enabled
*
* 因此,使用PIN_open返回的handle数据hKeyPins,并且为其指定相应的回调函数 Board_keyCallback
*
* PIN_setConfig(hKeyPins, PIN_BM_IRQ, Board_KEY_SELECT | PIN_IRQ_BOTHEDGES);实际来配置硬件GPIO的中断类型,配置为双边中断
*
* Util_constructClock(&keyChangeClock, Board_keyChangeHandler,
* KEY_DEBOUNCE_TIMEOUT, 0, false, 0); 用来创建一个定时器,实现消抖功能
*
*
* appKeyChangeHandler = CB; 注册消抖机制完成后执行的最终按键处理函数指针
*
* pra @ CB : 最终执行的按键处理函数指针
* return : NULL
*/
void board_key_init(keysPressedCB_t CB){
//分配操作用的handle空间
hKeyPins = PIN_open(&keyPins, keyPinsCfg);
//注册PIN相关的HWI事件发生后回调的函数
PIN_registerIntCb(hKeyPins, Board_keyCallback);
//配置实际GPIO的中断模式
PIN_setConfig(hKeyPins, PIN_BM_IRQ, Board_KEY_SELECT | PIN_IRQ_BOTHEDGES);
PIN_setConfig(hKeyPins, PIN_BM_IRQ, Board_KEY_UP | PIN_IRQ_BOTHEDGES);
PIN_setConfig(hKeyPins, PIN_BM_IRQ, Board_KEY_DOWN | PIN_IRQ_BOTHEDGES);
PIN_setConfig(hKeyPins, PIN_BM_IRQ, Board_KEY_LEFT | PIN_IRQ_BOTHEDGES);
PIN_setConfig(hKeyPins, PIN_BM_IRQ, Board_KEY_RIGHT | PIN_IRQ_BOTHEDGES);
//构建一个消抖用的定时器
Util_constructClock(&keyChangeClock, Board_keyChangeHandler,
KEY_DEBOUNCE_TIMEOUT, 0, false, 0);
//传入消抖后实际执行的最终处理函数指针
appKeyChangeHandler = CB;
}
/*
* brief PIN相关的HWI事件发生后回调的函数实体
* 使用PIN_getInputValue函数来判定是哪一个按键被按下,并且写入全局变量keysPressed中以记录。
* 判断完成后,打开定时器,等待KEY_DEBOUNCE_TIMEOUT时间之后,会自动执行Board_keyChangeHandler任务,这里实际上就是做了一个消抖处理
* Util_startClock(&keyChangeClock); 这里keyChangeClock是一个定时器实体,工作在one-shot模式,见datasheet CLOCK用法
*
* @PIN_Handle hPin, PIN_Id pinId,由系统调用者提供传参的格式
*
* return : NULL
*/
static void Board_keyCallback(PIN_Handle hPin, PIN_Id pinId){
keysPressed = 0;
if ( PIN_getInputValue(Board_KEY_SELECT) == 0 )
{
keysPressed |= KEY_SELECT;
}
if ( PIN_getInputValue(Board_KEY_UP) == 0 )
{
keysPressed |= KEY_UP;
}
if ( PIN_getInputValue(Board_KEY_DOWN) == 0 )
{
keysPressed |= KEY_DOWN;
}
if ( PIN_getInputValue(Board_KEY_LEFT) == 0 )
{
keysPressed |= KEY_LEFT;
}
if ( PIN_getInputValue(Board_KEY_RIGHT) == 0 )
{
keysPressed |= KEY_RIGHT;
}
Util_startClock(&keyChangeClock);
}
/*
* brief 最终的按键处理函数,运行init函数外部传入的处理函数指针
*
* pra @ UArg a0 按照回调函数指针参数要求写的,用来向外部传递键值
*
* return : NULL
*/
static void Board_keyChangeHandler(UArg a0)
{
if (appKeyChangeHandler != NULL)
{
// Notify the application
(*appKeyChangeHandler)(keysPressed);
}
}
创建board_key.h
#ifndef __BOARD_KEY__
#define __BOARD_KEY__
#include "stdint.h"
//KEY HARDWARE PIN INDEX
#define Board_KEY_SELECT IOID_11
#define Board_KEY_UP IOID_19
#define Board_KEY_DOWN IOID_12
#define Board_KEY_LEFT IOID_15
#define Board_KEY_RIGHT IOID_18
//KEY SOFTWARE INDEX
#define KEY_SELECT 0x0001
#define KEY_UP 0x0002
#define KEY_DOWN 0x0004
#define KEY_LEFT 0x0008
#define KEY_RIGHT 0x0010
// Debounce timeout in milliseconds
#define KEY_DEBOUNCE_TIMEOUT 200
typedef void (*keysPressedCB_t)(uint8 keysPressed);
extern void board_key_init(keysPressedCB_t CB);
#endif
可见 PIN_registerIntCb(hKeyPins, Board_keyCallback);函数将app与底层的HWI联结起来了。于是我们可以通过外部的函数来得知是否由外部中断进入。
到目前为止都仅仅是进行到硬件和软件之间的联结代码,也就是外部硬件事件来了,软件可以发现。没有用到TI-RTOS的TASK传消息机制。
目前做的消息传递是 H->S1 接下来就是 S1->S2->S3....
H ->S1 ->S2 (H 硬件消息 S 软件消息 ->消息传递)
在主函数中我们定义了如下函数作为指针传入board_key_init
board_key_init(SimpleBLEPeripheral_keyChangeHandler);
/*********************************************************************
* @fn SimpleBLEPeripheral_keyChangeHandler
*
* @brief Key event handler function
*
* @param keys - keypressed
*
* @return none
*/
void SimpleBLEPeripheral_keyChangeHandler(uint8 keys)
{
SimpleBLEPeripheral_enqueueMsg(SBP_KEY_CHANGE_EVT, keys);
}
在主函数中很明显创建了一个TASK共享的消息队列,并且定义了enqueue函数推入消息,这个enqueue消息有两个参数,第一个参数是用来分别消息的类型,是消息的唯一标志,第二个参数可用来传递一些其他信息。例如本例子中的键值。很显然在其他应用中会传递很大的数据,所以这里可以用ICALL来实现传大值。按键任务不需要,但使用不影响。
消息唯一标志已经定义了
// Internal Events for RTOS application
#define SBP_STATE_CHANGE_EVT 0x0001
#define SBP_CHAR_CHANGE_EVT 0x0002
#define SBP_PERIODIC_EVT 0x0004
#define SBP_CONN_EVT_END_EVT 0x0008
#define SBP_BLE_CHAR_CHANGE_EVT 0x0010
#define SBP_KEY_CHANGE_EVT 0x0020
前文说到,TI-RTOS TASK之间采用共享消息队列的方法来完成消息互传。
在这个应用的环境下,笔者认为HWI对应的一系列任务就是TASKA,他推入了有按键按下的消息进入队列
SimpleBLEPeripheral_enqueueMsg(SBP_KEY_CHANGE_EVT, keys);
而TASKB就是SimpleBLEPeripheral_task,这个TASK是一个最小的可以实现BLE从机角色的任务。换言之这个任务执行,则运行此代码的设备能实现BLE数据通讯的功能,是CC2640最基本的功能,这里的代码TI官方写好了,而且还包括主机Central 等其他角色相关代码。这里笔者在从机角色的基础上进行修改。
在TASKB的运行函数里有这样的几句
// Waits for a signal to the semaphore associated with the calling thread.
// Note that the semaphore associated with a thread is signaled when a
// message is queued to the message receive queue of the thread or when
// ICall_signal() function is called onto the semaphore.
ICall_Errno errno = ICall_wait(ICALL_TIMEOUT_FOREVER);
根据手册,笔者推断上述代码一定包含一个pending,取观察是否有POST出现,而TASKA在推入消息后,一定给出了一个POST,也许是在HWI的代码里实现的,因为我只是注册了回调函数,实际上系统内置的代码我并没有编写,有可能是已经固化的官方写的程序已经写好了这些,我们不得而知,只能猜测。
这里显然的说了 application wakes up 。
接下来在任务的死循环中找到了如下代码
while (!Queue_empty(appMsgQueue))
{
sbpEvt_t *pMsg = (sbpEvt_t *)Util_dequeueMsg(appMsgQueue);
if (pMsg)
{
// Process message.
SimpleBLEPeripheral_processAppMsg(pMsg);
// Free the space from the message.
ICall_free(pMsg);
}
}
发现此代码一直在检查appMsgQueue队列是否为空,如果空,那么就会用dequeue把报文推出来使用,从而进到我们自己定义的事件报文处理函数SimpleBLEPeripheral_processAppMsg(pMsg);。
/*********************************************************************
* @fn SimpleBLEPeripheral_processAppMsg
*
* @brief Process an incoming callback from a profile.
*
* @param pMsg - message to process
*
* @return None.
*/
static void SimpleBLEPeripheral_processAppMsg(sbpEvt_t *pMsg)
{
switch (pMsg->hdr.event)
{
case SBP_STATE_CHANGE_EVT:
SimpleBLEPeripheral_processStateChangeEvt((gaprole_States_t)pMsg->
hdr.state);
break;
case SBP_CHAR_CHANGE_EVT:
SimpleBLEPeripheral_processCharValueChangeEvt(pMsg->hdr.state);
break;
#ifdef QUNJIEBLE_CODE
case SBP_BLE_CHAR_CHANGE_EVT:
BLE_CharValueChangeEvt(pMsg->hdr.state);
case SBP_KEY_CHANGE_EVT:
SimpleBLEPeripheral_handleKeys(0, pMsg->hdr.state);
#endif
default:
// Do nothing.
break;
}
}
这里很显然,不仅仅TASKA可以触发TASKB的消息处理函数,还有很多其他的TASK也可以,是由各自TASK推入的消息类型来决定的,这里看出来pMsg->hdr.event存放着消息的类型,我们根据类型找到了SBP_KEY_CHANGE_EVT,并且执行了最终的执行函数SimpleBLEPeripheral_handleKeys(0, pMsg->hdr.state);
static void SimpleBLEPeripheral_handleKeys(uint8_t shift, uint8_t keys){
if(keys == KEY_LEFT){
LED_TURN_OFF();
}
if(keys == KEY_RIGHT){
LED_TURN_ON();
}
}
在这里我们操作,检测到左键关灯,右键开灯。
灯的代码很简单,配置方向,开灯高电平,关灯低电平
static PIN_State ledPins;
static PIN_Handle hledPins = NULL;
#define LED_PORT IOID_25
PIN_Config ledPinsCfg[] =
{
LED_PORT | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE
};
void board_led_init(void){
hledPins = PIN_open(&ledPins, ledPinsCfg);
}
void LED_TURN_OFF(void){
PIN_setOutputValue(hledPins, LED_PORT, 0);
}
void LED_TURN_ON(void){
PIN_setOutputValue(hledPins, LED_PORT, 1);
}
测试结果
按下按键LEFT,开灯,否则,关灯。
小结:
LED任务很简单,直接在上下文中进行,即在有需要的时候顺序执行即可。
BUTTON任务复杂,牵扯到了HWI TASK的消息传递,消息的传递过程包含两个类型
1 TASK 与 TASK之间的消息传递,非上下文传递,两个任务执行没有顺序关系。->
2 上下文消息传递,笔者认为,函数调用即上下文传递,函数调用本质上就是顺序执行的,有先后关系。->
HWI->Board_keyCallback->Board_keyChangeHandler->SimpleBLEPeripheral_keyChangeHandler
->SimpleBLEPeripheral_processAppMsg->SimpleBLEPeripheral_handleKeys
粉色函数扮演TASKA
绿色函数扮演TASKB 通过QUEUE共享消息
笔者将要在CC2640上开发很多功能,所以需要深入的了解TI-RTOS的结构机理,目前在努力的阅读datasheet,希望能把更多的设备运行在CC2640 上,这篇文章记录了笔者完成按键功能和led功能的内容。