本系列教程将结合ti推出的cc254x soc 系列,讲解从环境的搭建到蓝牙4.0协议栈的开发来深入学习蓝牙4.0的开发过程。教程共分为六部分,本文为第四部分:
第四部分知识点:
第十六节 协议栈led实验
第十七节 协议栈lcd显示
第十八节 协议栈uart实验
第十九节 协议栈五向按键
第二十节 协议栈flash数据存储
有关ti 的cc254x芯片介绍,可点击下面链接查看:
主流蓝牙ble控制芯片详解(1):ti cc2540
同系列资料推荐:
由浅入深,蓝牙4.0/ble协议栈开发攻略大全(1)
由浅入深,蓝牙4.0/ble协议栈开发攻略大全(2)
由浅入深,蓝牙4.0/ble协议栈开发攻略大全(3)
有关本文的工具下载,大家可以到以下这个地址:
朱兆祺forarm
第十六节 协议栈led实验
ti的协议栈中在hal层已经有了led的驱动,我们只需要针对我们的开发板进行配置即可,我们的开发板有两个led,分别对应p1.0和p1.1。这个在裸机开发的时候已经介绍了。
为了保持协议栈原有的代码不变,我们在ble-cc254x-1.4.0componentshal arget目录下新建一个文件夹,使它适应我们的开发板。
打开led实验工程ledexample,选择mt254xboard,并且在工程配置中要定义hal_led=true,下载到开发板运行,可以看到两个led同时在闪烁。
那我们的实现代码在哪里呢?其实在协议栈中实现这个很简单,在启动事件中我们调用了一个halledset函数,并且设置了两个led同时闪烁。
就是这么简单,协议栈已经把其它事情做好了,只需要我们调用设置函数即可。设置的模式总共有5种。
#define hal_led_mode_off 0x00 // 关闭led
#define hal_led_mode_on 0x01 // 打开led
#define hal_led_mode_blink 0x02 // 闪烁一次
#define hal_led_mode_flash 0x04 // 不断的闪烁,最多255次
#define hal_led_mode_toggle 0x08 // 翻转led状态
为了适应不同的需求,我们可能需要更改led的输出引脚,如图板级配置在hal_board_cfg.h文件中。
这里我们的开发板只有两个led,所以我们在这里根据开发板的实际情况修改相应的io口。
第十七节 协议栈lcd显示实验
打开lcd12864的实验工程,一样的在工程配置中打开lcd,选择mt254xboard然后直接编译下载,我们可以看到lcd上已经有显示了。
这些显示来自哪里呢?
在初始化函数中可以看到图中的函数调用,这里是将字符串显示到lcd的第一行。
在事件回调函数中可以看到这里将本机地址显示到第二行,将字符串initialized显示到第三行,但是为什么我们在第三行没有看到这行字符串呢?而显示的字符串是advertising ,这是因为系统启动后运行非常快,在我们还没反应过来的时候已经进入了广播状态,并且将原来的字符串覆盖了,所以我们最后只能看到advertising 了。
hallcdwritestring是将第一个参数指向的字符串显示到第二个参数指定第几行中,例如我们需要在第5行显示系统启动信息,我们可以在启动事件中,添加如下代码。
这里我们来介绍一下lcd驱动的实现,在hal_lcd.h文件中申明了以下函数,这些函数的功能都有英文注释,这里我就不再累述了。
/*
* initialize lcd service
*/
extern void hallcdinit(void);
/*
* write a string to the lcd
*/
extern void hallcdwritestring ( char *str, uint8 option);
/*
* write a value to the lcd
*/
extern void hallcdwritevalue ( uint32 value, const uint8 radix, uint8 option);
/*
* write a value to the lcd
*/
extern void hallcdwritescreen( char *line1, char *line2 );
/*
* write a string followed by a value to the lcd
*/
extern void hallcdwritestringvalue( char *title, uint16 value, uint8 format, uint8 line );
/*
* write a string followed by 2 values to the lcd
*/
extern void hallcdwritestringvaluevalue( char *title, uint16 value1, uint8 format1, uint16 value2, uint8 format2, uint8 line );
/*
* write a percentage bar to the lcd
*/
extern void hallcddisplaypercentbar( char *title, uint8 value );
协议栈中很多地方都调用了这些函数,我们如果要使我们的硬件能够兼容协议栈,被协议栈使用,就需要实现这些函数的定义,当然,为了适应我们的开发板,我已经实现了这些函数,实现都在hal_lcd.c中。
第十八节 协议栈uart实验
协议栈中已经用了串口的驱动,我们要做的只是对串口进行初始化,然后就可以进行串口数据的收发了。
用使用串口,第一步,需要打开使能串口功能,通过配置工程来实现,这里注意,我们现在不使用usb的cdc类来实现串口,所以hal_uart_usb=false。
hal_uart=true
hal_uart_usb=false
要使用串口必须先初始化相应的串口,那该如何初始化呢?在hal_uart.h文件中我们可以看到如下函数。
uint8 haluartopen(uint8 port, haluartcfg_t *config);
这个函数就是用来初始化串口的,这个函数有两个参数,第一个指定串口号,第二个是串口的配置参数。我们来看看这个结构体的定义:
typedef struct
{
bool configured; // 配置与否
uint8 baudrate; // 波特率
bool flowcontrol; // 流控制
uint16 flowcontrolthreshold;
uint8 idletimeout; // 空闲时间
haluartbufcontrol_t rx; // 接收
haluartbufcontrol_t tx; // 发送
bool intenable; // 中断使能
uint32 rxchrvdtime; // 接收数据时间
haluartcback_t callbackfunc; // 回调函数
}haluartcfg_t;
这个结构体成员很多,但是我们在使用串口的时候并不需要使用所有的成员。
void serial_init(void)
{
haluartcfg_t serialcfg = {0};
serialcfg.baudrate = hal_uart_br_115200; // 波特率
serialcfg.flowcontrol = hal_uart_flow_off; // 流控制
serialcfg.callbackfunc = serialcb; // 回调函数
serialcfg.intenable = true;
serialcfg.configured = true;
hallcdwritestring( “open uart0”, hal_lcd_line_5 ); // 在第5行显示启动信息
haluartopen(hal_uart_port_0, &serialcfg);
haluartwrite(hal_uart_port_0, “hello mt254xboard
”, osal_strlen(“hello mt254xboard
”));
}
在串口回调函数中我们只做一件事,将串口接收到的数据显示到lcd中并且原样的从串口输出。回调函数的实现如下:
static void serialcb( uint8 port, uint8 events )
{
uint8 rxbuf[64]={0};
if((events & hal_uart_tx_empty)||( events & hal_uart_tx_full )) // 发送区满或者空
{
return;
}
uint16 usrxbuflen = hal_uart_rxbuflen(hal_uart_port_0); // 读取接收据量
usrxbuflen = min(64,usrxbuflen);
uint16 readlen = haluartread(hal_uart_port_0, rxbuf, usrxbuflen);
haluartwrite(hal_uart_port_0, rxbuf, usrxbuflen);
}
实验现象,从实验现象中可以看到,一开始在串口中输出了一个标志字符串,然后我们通过串口发送了0123456789,然后数据原样的从串口输出了,这和我们预期的结果是一样的。
但是我们发现lcd上的显示和我们预期的不一样,lcd上只显示了6789,前面的数据并没有显示,这是怎么一回事呢?进行单步调试可以发现,我们发送一次数据,回调函数被回调了两次,第一次回调只接受到了012345,第二次回调接收到了6789,而在lcd上的显示第二次覆盖了第一次的显示,所以我们会看到这种现象,解决的办法,我们需要定义一个数据帧的时间间隔,当接收数据的间隔超过了此间隔就认为接收结束。
下面我们改写接收处理,我们在接收到数据后开启定时器,定时5ms这样,当接收间隔大于5ms后,我们就可以在定时事件中处理串口接收到的数据。
static void serialcb( uint8 port, uint8 events )
{
if((events & hal_uart_tx_empty)||( events & hal_uart_tx_full )) // 发送区满或者空
{
return;
}
uint16 usrxbuflen = hal_uart_rxbuflen(hal_uart_port_0); // 读取接收据量
if(usrxbuflen)
{
usrxbuflen = min(128,usrxbuflen);
uint16 readlen = haluartread(hal_uart_port_0, &serialrxbuf[rxindex], usrxbuflen); // 读取数据到缓冲区
rxindex += readlen;
readlen %= 128;
osal_start_timerex(simplebleperipheral_taskid, uart_event, 5); // 启动定时器
}
}
事件处理代码:
if ( events & uart_event )
{
hallcdwritestring( (char*)serialrxbuf, hal_lcd_line_6 ); // 在第5行显示启动信息
haluartwrite(hal_uart_port_0, serialrxbuf, osal_strlen(serialrxbuf));
osal_memset(serialrxbuf, 0, 128);
return (events ^ uart_event);
}
经过这样的处理后,可以发现我们刚刚的问题已经解决了。
到这里串口已经可以正常使用了,为了更加方便的使用串口,我在这里添加一个函数实现标准c中printf,这样更有利于我们输出。
int serialprintf(const char*fmt, 。。。)
{
uint32 ullen;
va_list ap;
char *pbuf = (char*)osal_mem_alloc(print_buf_len); // 开辟缓冲区
va_start(ap, fmt);
ullen = vsprintf(pbuf, fmt, ap); // 用虚拟打印函数实现
va_end(ap);
haluartwrite(hal_uart_port_0, (uint8*)pbuf, ullen); // 从串口0输出
osal_mem_free(pbuf); // 释放内存空间
return ullen;
}
我们可以像使用c标准中的printf来使用这个函数,例如我们将lcd的输出全部导向串口的输出,在hallcdwritestring的实现中添加串口输出代码,如下图:
重新编译并且烧录后可以看到lcd的输出和串口的输出是一样的了。
第十九节 协议栈五向按键
和前面几个一样,按键的驱动在协议栈中也已经有了,我们只需要做一些小的修改,使它适应我们的开发板即可。
1. 修改工程配置,使能按键功能。
2. 在我们的工程中要使用按键功能,仅仅打开配置选项是不够的。因为协议栈代码默认只有minidk开发板才有按键。
从这里可以看到(类似的地方有很多),如果要使能按键功能还需要定义cc2540_minidk,但是阅读整个协议栈你会发现,定义 cc2540_minidk后还会打开其它的功能,而那些功能并不是我们想要的,所以在这里我们使用另外一种方法来实现。我们定义我们的开发板也能使用按键功能,所以在工程配置中添加mt254xboard=true,然后在按键功能有宏开关的地方加入这个条件。具体位置参见代码。
按下相应的按键后可以看到串口输出相应的按键值。五向按键的工作原理在裸机开发的时候已经讲过了,在协议栈中已经有相应的驱动代码了,无需我们编写,只需要按照实际情况改写即可。例如我们的开发板每个按键对应的电压值和原来的值并不一样,所以我们这里改写了每个按键值的电压范围。
uint8 halgetjoykeyinput(void)
{
/* the joystick control is encoded as an analog voltage.
* read the joy_level analog value and map it to joy movement.
*/
uint16 adc;
uint8 ksave0 = 0;
uint8 ksave1;
/* keep on reading the adc until two consecutive key decisions are the same. */
do
{
ksave1 = ksave0; /* save previouse key reading */
adc = haladcread (hal_key_joy_chn, hal_adc_resolution_10);
if ((adc 》= 2) && (adc 《= 95)) // 85 right
{
ksave0 |= hal_key_right;
}
else if ((adc 》= 96) && (adc 《= 110)) // 101 cent
{
ksave0 |= hal_key_center;
}
else if ((adc 》= 111) && (adc 《= 140)) // 127 up
{
ksave0 |= hal_key_up;
}
else if ((adc 》= 141) && (adc 《= 200)) // 170 left
{
ksave0 |= hal_key_left;
}
else if ((adc 》= 201) && (adc 《= 300)) // 257 down
{
ksave0 |= hal_key_down;
}
} while (ksave0 != ksave1);
return ksave0;
}
第二十节 协议栈flash数据存储
cc254x自带了256k flash,这256k的储存空间不仅可以储存代码,也可以储存用户的数据,协议栈自带了snv管理代码,我们只需要学会使用即可。
snv的使用只有两个函数,分别是读函数osal_snv_read和写函数osal_snv_write,在snv的储存中,储存的每个数据都有一个唯一的id,snv也正是利用这个id来管理储存在flash中的数据,在ble的协议栈中,蓝牙自身数据储存用了一部分id,我们储存的数据id不可使用这些id,在bcomdef.h中有这些id的定义。
下面我们往snv中存入串口接收到的数据,然后开发板断电重启后读取出这串字符串并通过串口发送出去,来演示snv的断电保存。
首先我们定义一个我们储存数据的id,注意不能和已经有的定义冲突。
#define ble_nvid_user_cfg_start 0x80 //!《 start of the user configuration nv ids
#define ble_nvid_user_cfg_end 0x89 //!《 end of the user configuration nv ids
我们在启动事件中读取snv中0x80的值并通过串口输出读取结果,如果读取成功,则会将读取结果打印到pc端,如果读取失败,则会提示读取失败。
在串口接收事件中将接收到的数据存入snv中,并且也进行相应的提示。
将工程编译下载后,可以看到现象如下:
第一次上电可以看到,提示读取数据失败了,说明第一次运行时是没有存储数据的,接下来我们通过串口发送字符串 mt254xboard snv test字符串。
可以看到成功的将我们发送过去的字符存入了snv中,那是否成功存入呢?我们将开发板断电后重启,看看第二次上电是否能够读取出我们存入的数据。
重启后可以发现我们成功的读取出了第一次存入的数据,说明我们成功的将数据存入了snv中。
区块链技术的六层技术模型介绍
蔡勇:未来十年将是中国MEMS产业黄金十年
旧款苹果iPhone电池续航实测
2019年有什么特征表示了SD-WAN很重要
行业竞争格局集中 电容电感连接器出货额下滑
由浅入深,蓝牙4.0/BLE协议栈开发攻略大全(4)
边缘计算的前景与挑战
Google Pixel 4a在Android 11上运行不佳
AI和大数据将如何改变医疗保健?
宁德时代/松下/LG化学/三星SDI数据对比 全球动力电池市场的需求激增
中科院计划斥资3000万,推动5G芯片产业项目
基于FPGA和高速A/D转换芯片ADC08D1500的相关系统设计
wifi通信协议有哪些 wifi和蜂窝有什么区别
M5156-000002-002BG船舶压力传感器应用
虚拟机VMware使用教程
船舶零件或完整结构3D表面分析验证零件的正确尺寸及其偏差
特斯拉将实现全部零部件的国产化替代
“三合一”北斗双模单芯片为进军手机市场
人工智能有望推动智能经济的发展
诺基亚X7-00真机曝光