电路设计简介
- CC2640 的 RF 差分线越短越好,做差分 100Ω 阻抗匹配。
- 天线部分阻抗 50 欧姆匹配,本次设计采用了陶瓷天线 AN9520-245 减少天线面积。
- 蓝牙芯片在底层,阻抗参考平面第三层,在天线部分下方覆铜(GND),使用嘉立创的阻抗匹配计算器计算线宽。
- 巴伦采用分立设计,参考官方文档。ST 公司的 BlueNRG-2 BLE 芯片可采用 BALF-NRG-02D3 巴伦封装。
- 天线部分转角做弧线。
- 可将天线线路做包地处理,减少信号干扰。
嵌入式开发
这里说的所有内容都是基于 ProjectZero 项目进行二次开发的
关于在 RTOS 中创建 Task 的一点灵感
CC2640 中分配内存主要使用的是:
void *ICall_malloc(uint_least16_t size);
配套用于释放内存的函数是:
void ICall_free(void *msg);
在这个灵感中会用到动态内存的创建和释放,所以可以稍微包装下:
mem.c 文件
#include "mem.h"
#include <icall.h>
void* c_malloc(uint_least16_t size) {
return ICall_malloc(size);
}
void c_free(void* ptr) {
ICall_free(ptr);
}
因为很多时候创建任务的一些结构体还有 Task 所需的缓存大小都差不多,主要是想偷偷懒,所以提前分配好一些 Task 的结构体和相关的缓冲区是否能少写一点代码呢?(我不确定这是不是个好办法但确实可以偷懒)
这是 task.c 的实现方式
#include "task.h"
#include "mem.h"
//#include <uartlog/UartLog.h>
static TASK_FACTORY _G_TASK_FACTORY = {0, 0, 0, 0, 0, false};
void task_init(uint32_t task_num, uint32_t stack_size) {
if (_G_TASK_FACTORY.is_init) return;
_G_TASK_FACTORY.is_init = true;
_G_TASK_FACTORY.task_capacity = task_num;
_G_TASK_FACTORY.stack_capacity = stack_size;
_G_TASK_FACTORY.tasks = (Task_Struct*)c_malloc(task_num * sizeof(Task_Struct));
_G_TASK_FACTORY.stack = (uint8_t*)c_malloc(task_num * stack_size);
}
int task_run(uint8_t priority, Task_FuncPtr func) {
if (!(_G_TASK_FACTORY.is_init) || _G_TASK_FACTORY.task_size >= _G_TASK_FACTORY.task_capacity) {
return -1;
}
Task_Params taskParams;
Task_Params_init(&taskParams);
taskParams.stack = _G_TASK_FACTORY.stack + _G_TASK_FACTORY.stack_capacity * _G_TASK_FACTORY.task_size;
taskParams.stackSize = _G_TASK_FACTORY.stack_capacity;
taskParams.priority = priority - 1;
Task_construct(_G_TASK_FACTORY.tasks + _G_TASK_FACTORY.task_size, func, &taskParams, NULL);
_G_TASK_FACTORY.task_size++;
return 0;
}
以闪烁 LED 灯为例使用这两个函数:
led_task.c
#include "led_task.h"
#include "../core/task.h"
#include "ti/drivers/PIN.h"
#include "driverlib/ioc.h"
#include <ti/sysbios/knl/Clock.h>
#include "../events/event.h"
#include <ti/sysbios/knl/Event.h>
#include <uartlog/UartLog.h>
static PIN_State ledPin;
static PIN_Handle hLedPin = NULL;
static PIN_Config ledPinCfg[] =
{
IOID_16 | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE
};
static void led_task_fxn(UArg a0, UArg a1)
{
hLedPin = PIN_open(&ledPin, ledPinCfg);
for(;;) {
PIN_setOutputValue(hLedPin, IOID_16, 0);
Task_sleep(((500) * 1000) / Clock_tickPeriod);
PIN_setOutputValue(hLedPin, IOID_16, 1);
Task_sleep(((500) * 1000) / Clock_tickPeriod);
}
}
void led_task_run() {
task_run(2, led_task_fxn);
}
然后在 app.c 中初始化 Task 任务池(Task Pool,管理任务相关的结构体和缓存)
#include "tasks/led_task.h"
task_init(3, 512);
led_task_run();
关于多任务开发需要注意的问题:
中断级别的一点误解:有时候发现任务无法正常运行,但是调高中断级别后能够正常运行,这时候如果多创建几个任务又不正常了。这种情况可能不是中断级别的问题,而是缓冲区配置太大造成的。
ICALL_MAX_NUM_ENTITIES
和ICALL_MAX_NUM_TASKS
的配置,其中ICALL_MAX_NUM_TASKS
(默认值:2),定义位置在应用的ICall/icall.c
文件中,有这样一段描述:
#ifndef ICALL_MAX_NUM_ENTITIES
/**
* Maximum number of entities that use ICall, including service entities
* and application entities.
* The value may be overridden by a compile option.
* Note that there are at least,
* Primitive service, Stack services along with potentially generic
* framework service for the stack thread.
*/
#define ICALL_MAX_NUM_ENTITIES 6
#endif
#ifndef ICALL_MAX_NUM_TASKS
/**
* Maximum number of threads which include entities.
* The value may be overridden by a compile option.
*/
#define ICALL_MAX_NUM_TASKS 6
#endif
*/
所以如果需要创建多个 Task 的话最好将ICALL_MAX_NUM_TASKS
设大一些,目前我配置的是:6
还有另一处在:TOOLS/defines/ble5_project_zero_cc2640r2lp_app_FlashROM_StackLibrary.opt
,里边有个配置项:-DICALL_MAX_NUM_TASKS=6
,这里我也把它设大一些。
定时器的使用:
步骤如下:
定义定时器事件和结构体变量定义
#define CAW_TIMEOUT_EVT Event_Id_10 // 超时事件
#define CAW_TIMEOUT_EVT_INERVAL 500 // 定义超时时间
static Clock_Struct periodicClock; // 定时器结构体
创建定时器处理函数
static void _timeoutHandler(UArg arg)
{
// 自定义参数arg中存放CAW_TIMEOUT_EVT值
if (arg == CAW_TIMEOUT_EVT) {
// 触发CAW_TIMEOUT_EVT事件
Event_post(syncEvent, arg);
}
}
编写事件处理函数
if(events) {
// 如果当前事件中含有SBP_CAW_PERIODIC_EVT则进入处理
if (events & SBP_CAW_PERIODIC_EVT) {
//! TODO
// 加入需要超时处理的代码
// ......
// 这里需要重启定时器,否则定时器只工作一次
Util_startClock(&periodicClock);
}
}
配置定时器并启动,进入超时处理流程
// 配置定时器
Util_constructClock(&periodicClock, _timeoutHandler,
CAW_TIMEOUT_EVT_INERVAL, 0, false, CAW_TIMEOUT_EVT);
// 初次启动
Util_startClock(&periodicClock);
队列的使用(Queue)
步骤如下:
创建队列消息结构和队列结构体定义
// 队列消息结构体
typedef struct {
uint8_t event;
void *pData;
} pzMsg_t;
// 创建队列变量
static Queue_Struct msgQueue;
static Queue_Handle msgQueueHandle;
创建队列处理函数
if(events) {
// 通过while循环将队列中的消息消耗完
while(!Queue_empty(msgQueueHandle)) {
pzMsg_t *pMsg = (pzMsg_t *)Util_dequeueMsg(msgQueueHandle);
if(pMsg) {
//! TODO
// 处理消息
ICall_free(pMsg);
}
}
}
初始化队列
Queue_construct(&msgQueue, NULL);
msgQueueHandle = Queue_handle(&msgQueue);
-
将消息插入队列,其中第一个参数可以是自定义的事件,第二个参数是一个数据结构体的指针变量
static status_t enqueueMsg(uint8_t event, void *pData) {
uint8_t success;
pzMsg_t *pMsg = ICall_malloc(sizeof(pzMsg_t));
if(pMsg) {
pMsg->event = event;
pMsg->pData = pData;
success = Util_enqueueMsg(g_msgQueueHandle, g_syncEvent, (uint8_t *)pMsg);
return (success) ? SUCCESS : FAILURE;
}
return(bleMemAllocError);
}
处理蓝牙接收到的数据
在Application/services
目录下存放了很多服务,其中data_service.c
提供了数据传输的基本能力,在project_zero.c
中注册了一些服务的回调函数,比如
static DataServiceCBs_t Message_ServiceCBs =
{
.pfnChangeCb = DataService_ValueChangeCB, // Characteristic value change callback handler
.pfnCfgChangeCb = DataService_CfgChangeCB, // Noti/ind configuration callback handler
};
因为我们需要处理的是接收到的数据,所以只关注DataService_ValueChangeCB
回调函数就足够了,看看DataService_ValueChangeCB
长什么样的:
static void DataService_ValueChangeCB(uint16_t connHandle,
uint8_t paramID, uint16_t len,
uint8_t *pValue)
{
// See the service header file to compare paramID with characteristic.
Log_info1("(CB) Data Svc Characteristic value change: paramID(%d). "
"Sending msg to app.", paramID);
pzCharacteristicData_t *pValChange =
ICall_malloc(sizeof(pzCharacteristicData_t) + len);
if(pValChange != NULL) {
pValChange->svcUUID = MESSAGE_SERVICE_SERV_UUID;
pValChange->paramID = paramID;
memcpy(pValChange->data, pValue, len);
pValChange->dataLen = len;
// 此处会向消息队列中投递一个PZ_SERVICE_WRITE_EVT的事件,并且带着
// pValChange值
if(enqueueMsg(PZ_SERVICE_WRITE_EVT, pValChange) != SUCCESS) {
ICall_free(pValChange);
}
}
}
所以应该去ProjectZero_processApplicationMessage
队列处理函数中看看发生了什么:
static void ProjectZero_processApplicationMessage(pzMsg_t *pMsg) {
// ......
switch(pMsg->event) {
// 因为投递的到队列中的消息使用的事件是:PZ_SERVICE_WRITE_EVT
// 所以这里我们只关注PZ_SERVICE_WRITE_EVT就可以了
case PZ_SERVICE_WRITE_EVT:
switch(pCharData->svcUUID) {
case DATA_SERVICE_SERV_UUID:
// 这个函数就是处理蓝牙接收到消息的函数了
DataService_ValueChangeHandler(pCharData);
break;
}
break;
// ......
}
// ......
}
可以看到最终调用了DataService_ValueChangeHandler
函数处理数据,继续跟踪:
void DataService_ValueChangeHandler(
pzCharacteristicData_t *pCharData)
{
static uint8_t received_string[DS_STRING_LEN] = {0};
switch (pCharData->paramID)
{
case DS_STRING_ID:
memset(received_string, 0, DS_STRING_LEN);
memcpy(received_string, pCharData->data,
MIN(pCharData->dataLen, DS_STRING_LEN - 1));
// 将数据通过串口发送出去
Log_info3("Value Change msg: %s %s: %s",
(uintptr_t) "Data Service",
(uintptr_t) "String",
(uintptr_t)received_string);
//! TODO
// 这里可以添加消息处理流程
break;
// 此处省略Stream(流)处理
default:
return;
}
}
串口日志
需要包含头文件:#include <uartlog/UartLog.h>
# define Log_info0(fmt)
# define Log_info1(fmt, a0)
# define Log_info2(fmt, a0, a1)
# define Log_info3(fmt, a0, a1, a2)
# define Log_info4(fmt, a0, a1, a2, a3)
# define Log_info5(fmt, a0, a1, a2, a3, a4)
# define Log_warning0(fmt)
# define Log_warning1(fmt, a0)
# define Log_warning2(fmt, a0, a1)
# define Log_warning3(fmt, a0, a1, a2)
# define Log_warning4(fmt, a0, a1, a2, a3)
# define Log_warning5(fmt, a0, a1, a2, a3, a4)
# define Log_error0(fmt)
# define Log_error1(fmt, a0)
# define Log_error2(fmt, a0, a1)
# define Log_error3(fmt, a0, a1, a2)
# define Log_error4(fmt, a0, a1, a2, a3)
# define Log_error5(fmt, a0, a1, a2, a3, a4)
可以清楚的看到支持 3 个日志级别分别是:info, warning 和 error,宏名最后的数字代表的是可变参数的数量,如果没有可变参数,那就选择Log_info0
就可以了。
如何拦截 ProjectZero 中的事件处理
定义如下一个函数
xdc_UInt Caw_Event_pend(ti_sysbios_knl_Event_Handle __inst, xdc_UInt andMask, xdc_UInt orMask, xdc_UInt32 timeout) {
xdc_UInt events = ti_sysbios_knl_Event_pend(__inst, andMask, orMask | SBP_CAW_ALL_EVENTS, timeout);
// 此处为自定义事件处理函数
Event_processer(events);
return events;
}
然后在project_zero.c
文件中搜索Event_pend
,并使用Caw_Event_pend
函数替换Event_pend
函数调用,就完成了拦截
static void ProjectZero_taskFxn(UArg a0, UArg a1)
{
// Initialize application
ProjectZero_init();
Caw_Event_init(selfEntity, syncEvent);
// Application main loop
for(;; ) {
uint32_t events;
// 拦截ProjectZero的事件处理
events = Caw_Event_pend(syncEvent, Event_Id_NONE, PZ_ALL_EVENTS,
ICALL_TIMEOUT_FOREVER);
if(events) {
// ......
}
}
其他问题:
使用 Code Composer Studio + XDS100 V3.0 仿真时报错:
错误如下:
An error occurred while hard opening the controller.
-----[An error has occurred and this utility has aborted]--------------------
This error is generated by TI's USCIF driver or utilities.
The value is '-183' (0xffffff49).
The title is 'SC_ERR_CTL_CBL_BREAK_FAR'.
The explanation is:
The controller has detected a cable break far-from itself.
The user must connect the cable/pod to the target.
解决方式:连线错误,正确的接线方案如下:
调试器引脚 |
芯片引脚 |
1 |
TMS 引脚 |
11 |
TCK 时钟 |
15 |
SRSTN 芯片复位引脚 |
4 |
GND 接地 |
5 |
3.3V |
8 |
GND 接地 |
使用 UniFlash 或 SmartRF Flash Programmer 2 烧录后,将 XDS100 V3.0 的 USB 线拔掉,电路板断电重新上电后 CC2640 芯片不工作,但是将 XDS100 V3.0 插上 USB 供电后 CC2640 就能够正常工作
解决方式:不要拔 USB 线,直接将 XDS100 V3.0 与电路板的连接线拔掉,这时候电路板断电再上电后 CC2640 能够正常工作。
CC2640 插着调试器能够正常运行,复位后不能正常运行
解决方式:切换老版本的 SimpleLink CC2640R2 SDK 的版本
CC2540R2L 不支持 Sensor controller
注意:
本人刚入门硬件设计和嵌入式开发也刚接触 CC2640 蓝牙芯片不久,文中可能会出现一些问题,我自己在项目中修改了很多 ProjectZero 的文件,所以本文的变量名和函数名是我零时改的可能会出现差异。