This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

CC2540/CC2541蓝牙4.0BLE协议栈开发(连载)

Other Parts Discussed in Thread: CC2540, CC2430, CC2530, CC2541

    温馨提示:本连载帖子图形文字密切结合,每一步操作都有截图,建议大家注册登录本论坛,这样方能看到图片。

   低功耗蓝牙(BluetoothLow Energy),简称BLE。蓝牙低能耗无线技术利用许多智能手段最大限度地降低功耗。

  蓝牙2.1+EDR/3.0+HS版本(通常指“标准蓝牙技术”)与蓝牙低能耗(BLE)技术有许多共同点:它们都是低成本、短距离、可互操作的鲁棒性无线技术,工作在免许可的2.4GHz ISM射频频段。

  不过它们之间有一个重要区别:蓝牙低能耗技术从一开始就设计为超低功耗(ULP)无线技术,而标准蓝牙技术主要是能够构成“低功耗的”无线连接。

标准蓝牙技术是一种“面向连接”的无线技术,具有固定的连接时间间隔,因此是移动电话连接无线耳机等高活动连接的理想之选。相反,蓝牙低能耗技术采用可变连接时间间隔,这个间隔根据具体应用可以设置为几毫秒到几秒不等。另外,因为BLE技术采用非常快速的连接方式,因此平时可以处于“非连接”状态(节省能源),此时链路两端相互间只是知晓对方,只有在必要时才开启链路,然后在尽可能短的时间内关闭链路。

BLE技术的工作模式非常适合用于从微型无线传感器(每半秒交换一次数据)或使用完全异步通信的遥控器等其它外设传送数据。这些设备发送的数据量非常少(通常几个字节),而且发送次数也很少(例如每秒几次到每分钟一次,甚至更少)。

    蓝牙低能耗架构共有两种芯片构成:单模芯片和双模芯片。蓝牙单模器件是蓝牙规范中新出现的一种只支持蓝牙低能耗技术的芯片——是专门针对ULP操作优化的技术的一部分。蓝牙单模芯片可以和其它单模芯片及双模芯片通信,此时后者需要使用自身架构中的蓝牙低能耗技术部分进行收发数据。双模芯片也能与标准蓝牙技术及使用传统蓝牙架构的其它双模芯片通信。

    双模芯片可以在目前使用标准蓝牙芯片的任何场合使用。这样安装有双模芯片的手机、PC、个人导航设备(PND)或其它应用就可以和市场上已经在用的所有传统标准蓝牙设备以及所有未来的蓝牙低能耗设备通信。然而,由于这些设备要求执行标准蓝牙和蓝牙低能耗任务,因此双模芯片针对ULP操作的优化程度没有像单模芯片那么高。

    单模芯片可以用单节钮扣电池(如3V、220mAh的CR2032)工作很长时间(几个月甚至几年)。相反,标准蓝牙技术(和蓝牙低能耗双模器件)通常要求使用至少两节AAA电池(电量是钮扣电池的10至12倍,可以容忍高得多的峰值电流),并且更多情况下最多只能工作几天或几周的时间(取决于具体应用)。注意,也有一些高度专业化的标准蓝牙设备,它们可以使用容量比AAA电池低的电池工作。

蓝牙4.0已经走向了商用,在最新款的Xperia Z、Galaxy S3、S4、Note2、SurfaceRT、iPhone 5、iPhone 4S、魅族MX3、Moto Droid Razr、HTC One X、小米手机2、The New iPad、iPad 4、 MacBook Air、Macbook Pro,Nokia Lumia系列以及台商ACER AS3951系列/Getway NV57系列,ASUS UX21/31三星NOTE系列上都已应用了蓝牙4.0技术。


    接下来我们从环境的搭建到蓝牙4.0协议栈的开发来深入学习蓝牙4.0的开发过程。

    本课程所有程序和文档资料均可在以下网盘下载:
pan.baidu.com/.../link


目录导航:

第一节 BLE开发环境的搭建

第二节 BLE快速体验

第三节 创建IAR工程-点亮LED

第四节 控制LED

第五节 LCD12864显示

第六节 独立按键之查询方式

第七节 独立按键之中断方式

第八节 CC254x内部温度传感器温度采集

第九节 五向按键

第十节 蜂鸣器

第十一节 串口通信

第十二节 Flash的读写

第十三节 BLE协议栈简介

第十四节 OSAL工作原理

第十五节 BLE蓝牙4.0协议栈启动分析

第十六节 协议栈LED实验

第十六节 协议栈LCD显示

第十七节 协议栈UART实验

第十八节 协议栈五向按键

第十九节 协议栈Flash数据存储

第二十节 DHT11温湿度传感器

第二十一节 蓝牙协议栈之从机通讯

第二十二节 蓝牙协议栈主从一体之主机通讯

第二十三节 OAD空中升级

第二十四节 SBL串口升级

第二十五节 UBL-USB升级

第二十六节 MT-iBeacon基站使用iPhone空中升级

第二十七节 MT-iBeacon基站在PC端实现OAD空中升级

第二十八节 MT-iBeacon基站关于LightBlue软件的使用

第二十九节 如何使用MT-USBDongle的透传功能


  • 第一节  BLE开发环境的搭建
    1.1  硬件准备

        要进行BLE的开发,首先我们需要一个硬件环境。

    (1)  MT254xBoard开发板(最好有两块,方便进行数据收发实验);

    (2)  USBDongle-BLE抓包工具(多个固件,一个硬件多种用途),协议开发时辅助我们分析数据包;

    (3)  开发必备CC-Debug,用于下载和调试程序;

        在后期的学习中,这些工具我们都会使用到!

    1.2  BLE协议栈的安装

        我们使用的是最新版本的协议栈BLE-CC254x-1.4.0,首先在配套的资料文件夹中的tools文件夹下找到BLE-CC254x-1.4.0.exe文件。

        我们提供了一个安装包和一个免安装的源码,根据我的开发经验,建议使用安装包安装到C盘,直接使用免安装源码在后期的开发中会遇到一些莫名其妙的问题。下面开始安装协议栈,安装方式很简单,和安装软件一样,直接下一步到底即可。


    开始安装:

    同意安装:

    选择C盘:

        按照上述步骤,直到完成安装即可。在安装的最后阶段,默认的会安装Btool。

    安装BTool:

    安装完成:

    至此,说明我们已经成功安装了协议栈。完成后将会出现说明文件。

    在说明文件中我们可以看到,这个版本的协议栈需要使用IAR for 8051 8.20.2版本的软件。

    注:如果使用的是Win8以上的系统建议使用IAR for 8051 8.30.2版本的软件,安装方式和8.20.2是一样的。


        下面我们就开始安装这个版本的软件。

    1.3 IAR安装

        在配套的文件目录下找到如下文件。

    安装IAR:

    点击是开始安装Dongle驱动

    这样IAR的安装就完成了。先开启软件来体验一下安装成果吧!

    1.4 安装烧写软件

    解压后可以看到如下文件:

    安装此软件即可,安装步骤:

    直接下一步到最终即可。

        至此,我们目前需要用到的开发软件就安装完成了,来个全家福吧!


  • 第二节  BLE快速体验

        经过前面的安装,我们的开发环境已经搭建好了,现在我们先来体验一下BLE,给自己点动力,comeon!使用SmartRFFlash Programmer烧写从机固件:CC2540_SmartRF_SimpleBLEPeripheral.hex,烧写方法见SmartRF Flash Programmer的使用章节。

        协议栈默认自带了一些已经编译好的文件,可以直接烧写,具体路径如下图:

         从机固件路径:

        读取设备的IEEE地址:

        烧写完成后,如果你有支持Ble的手机或平板就可以搜索到设备了,或者使用本公司开发的USBDongle(抓包固件或HostTestRelease固件)也可以搜索到设备,具体的使用可以阅读相应的产品使用手册,我这里用andriod平板搜索:

        通过MAC地址可以知道我们的设备已经在正常的广播了,我这里使用本公司开发的andriod端软件TruthBlue可以正常搜索到我们的设备。如果用户手上有支持BLE的设备并且系统在andriod4.3以上也可以安装我们的这个软件。

        连接上设备后如图,这里我们不要求大家能够看懂这些,这里仅仅是为了体验,后面的章节中我们会详细的讲述这些知识。



  • 表示 很期待更多的介绍,支持!!!

  • 第三节  创建IAR工程-点亮LED

        经过前面的准备工作,这章开始我们开始正式的开发过程。万事开头难,针对MT254xboard开发板的详细介绍参见《MT254xBoard-V1.0-硬件手册.pdf》,在这里我不做详述。

        这个教程是为有一定51基础和C基础的人准备的,如果读者这方面还欠缺,请找相关方面的书籍恶补一下。CC2540的本质就是一个8051的单片机,所以我们裸机开发就可以作为一个51单片机来开发,裸机开发的目的是为了让大家熟悉整个硬件以及开发环境,这并不是我们的最终目的,但这是一个必须的过程,为后面开发协议栈奠定基础。

        打开我们前面安装的IAR软件,创建一个新的工程:

    因为我们使用的CC2540是增强型51单片机,这里我们创建一个空的8051工程,具体配置选项如

    选择目录保存工程:

        我们这里创建一个最简单的例程,点亮一个LED,这个例程就像我们学习每种编程语言是都是先来个Hello World!。虽然简单,但是能够让我们最快的掌握一个开发环境的使用。

    新的工程为空工程,没有任何文件,我们这里新建一个文件并且保存为C文件。

    添加文件到工程:

        保存WorkSpace,在IAR中每个工程都必须要有一个Workspace,而且一个Workspace中可以有多个工程,所以这里我也必须要保存一个Workspace,点击file->save Workspace As就会弹出如下对话框,这里和保存文件一样需要对这个WorkSpace命名,我们这里一样取名LED。

     接下来我们需要对工程进行一些配置,使它适应我们的CPU。在工程处右击,进入配置界面。

    CPU配置:

        这里我们第一个要做的就是选择我们的CPU,我们使用的是TI公司生产的CC2540F256,所以这里选择CC2540F256。配置好CPU后,我们还需要配置编译输出的文件格式,选择到Linker选项,配置如下图:

    debug选项:

    选项配置:

         经过这些配置后,我们可以开始编码了,下面开始编写我们的第一个代码,功能是点亮2个LED,开发板上有两个LED灯,分别对应P1.0和P1.1。

        代码如下,可能觉得都是注释,这里我还是建议大家有一个好的编码风格,在开发大项目时就能够看到它的优势。

    /******************************************************************************
    
                      版权所有 (C), 2013-2020, 深圳市馒头科技有限公司
    
     ******************************************************************************
      文 件 名   : LED.c
      版 本 号   : V1.0
      作    者   : 朱兆祺
      生成日期   : 2014年6月6日
      功能描述   : 点亮一个LED
      函数列表   :
                  main
      修改历史   :
      1.日    期   : 2014年6月6日
        作    者   : 朱兆祺
        修改内容   : 创建文件
    
    ******************************************************************************/
    
    /*----------------------------------------------*
     * 包含头文件                                   *
     *----------------------------------------------*/
    #include <ioCC2540.h>
    
    #include "delay.h"
    
    /*****************************************************************************
     函 数 名  : main
     功能描述  : 主函数,C程序入口
     输入参数  : void
     输出参数  : 无
     返 回 值  : 
    
     修改历史      :
      1.日    期   : 2014年6月6日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    int main(void)
    {
        P1SEL &= ~0X03;     // 将P1.1、0设置为IO功能
        P1DIR |= 0X03;      // 设置P1.1、0为输出功能
        
        while(1)
        {
        P1  = (P1 & 0XFC) | 0X01;         // 设置P1.0输出高电平
        }
        return 0;
    }

    编写好代码后,就可以编译下载到开发板上了。点击图中所示图标全速运行。

    根据原理图,P1.0对应的是LED2,这里我们能够看到LED2处于点亮的状态。

        根据CC254X的数据手册,我们可以很快知道P1SEL是设置IO功能,P1DIR是设置输入输出。至于为什么程序是这么写,我们来看下,CC254X芯片的P1口一共有8个IO口,那就是说刚刚好由两位十六进制进行控制:1111 1111(FF),这里仅仅是LED1和LED2,也就是P1.1和P1.0两个IO口,为了不影响其他引脚的使用,我们这里巧妙使用与或控制其功能。比如:P1 = (P1 & 0XFC) | 0X01;    P1与上1111  1100,这样不影响其他引脚的基础上,清除了P1.0和P1.1的输出,再或上0X01,这样将P1.0设置为高电平,根据原理图,高电平是点亮LED2.


  • 第四节  控制LED

    上一节点亮了单个LED灯,我们这堂课接着控制LED灯。这堂课我们要完成的是LED闪烁10次,蜂鸣器响1s钟。这里我们先使用延时函数进行。
        我们的程序一定要做到结构清晰,可移植性强,阅读性高。程序设计不仅仅是实现了功能,如果那样的代码,那只有你自己可以看懂,是一手垃圾。真正的漂亮代码具有阅读性高、可移植性强、代码规范性好。

    delay.h:

    /******************************************************************************
    
                      版权所有 (C), 2013-2020, 深圳市馒头科技有限公司
    
     ******************************************************************************
      文 件 名   : delay.h
      版 本 号   : V1.0
      作    者   :  朱兆祺
      生成日期   : 2014年06月07日
      功能描述   : 主函数
      函数列表   :
                  
      修改历史   :
      1.日    期   : 2014年06月07日
        作    者   :  朱兆祺
        修改内容   : 创建文件
    
    ******************************************************************************/
    #ifndef  __DELAY_H__
    #define  __DELAY_H__
    
    
    /*****************************************************************************
     函 数 名  : Delay1ms
     功能描述  : 延时函数
     输入参数  : unsigned int uiDelay:延时1ms的数量
     输出参数  : 无
     返 回 值  : 
    
     修改历史      :
      1.日    期   : 2014年6月7日
        作    者   :  朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    extern void Delay1ms(unsigned int uiDelay);
    
    #endif
    
    
    /* end  file */

    延时函数的执行程序delay.c:

    /******************************************************************************
    
                      版权所有 (C), 2013-2020, 深圳市馒头科技有限公司
    
     ******************************************************************************
      文 件 名   : delay.c
      版 本 号   : V1.0
      作    者   :  朱兆祺
      生成日期   : 2014年06月07日
      功能描述   : 主函数
      函数列表   :
                  
      修改历史   :
      1.日    期   : 2014年06月07日
        作    者   :  朱兆祺
        修改内容   : 创建文件
    
    ******************************************************************************/
    /* 包含delay延时的头文件 */
    #include "delay.h"
    
    
    /*****************************************************************************
     函 数 名  : Delay1ms
     功能描述  : 延时函数
     输入参数  : unsigned int uiDelay:延时1ms的数量
     输出参数  : 无
     返 回 值  : 
    
     修改历史      :
      1.日    期   : 2014年6月7日
        作    者   :  朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    void Delay1ms(unsigned int uiDelay)
    {
        unsigned int i;
        
        for ( ; uiDelay > 0; uiDelay--)
        {
            /* 大约延时1ms */
            for (i = 0; i < 320; i++);
        }
    }
    
    
    /* end  file */

    主函数其实也很简单:

    /******************************************************************************
    
                      版权所有 (C), 2013-2020, 深圳市馒头科技有限公司
    
     ******************************************************************************
      文 件 名   : main.c
      版 本 号   : V1.0
      作    者   :  朱兆祺
      生成日期   : 2014年06月06日
      功能描述   : 主函数
      函数列表   :
                  
      修改历史   :
      1.日    期   : 2014年06月06日
        作    者   :  朱兆祺
        修改内容   : 创建文件
    
    ******************************************************************************/
    /* 包含CC254X的头文件 */
    #include <ioCC2540.h>
    #include "delay.h"
    
    /*****************************************************************************
     函 数 名  : main
     功能描述  : 主函数
     输入参数  : 无
     输出参数  : 无
     返 回 值  : 
    
     修改历史      :
      1.日    期   : 2014年6月6日
        作    者   :  朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    
    int main(void)
    {
        /* 控制LED灯闪烁 */
        unsigned char i;
        /* 驱动无源蜂鸣器 */
        unsigned int j;
        
      
        /* 将P1.0、P1.1设置为IO口 */
        P1SEL &= ~0x03;
        /* 将P1.0、P1.1设置为IO口的输出 */
        P1DIR |= 0x03;
        
        /* 将P2.0设置为IO口 */
        P2SEL &= ~0x01;
        /* 将P2.0设置为IO口输出 */
        P2DIR |= 0x01;
        
        /* 主循环 */
        while(1)
        {
            /* LED1,LED2闪烁10次 */
            for (i = 0; i < 10; i++)
            {
                /* P1.0----LED2,P1.1----LED1 */
                /* P1.0,P1.1输出高电平,即点亮LED2,LED1 */
                /* FC :  1111 1100*/
                P1 = (P1 & 0xFC) | 0x03;
                Delay1ms(1000);
            
                /* P1.0,P1.1输出低电平,即熄灭LED2,LED1 */
                /* FC :  1111 1100*/
                P1 = (P1 & 0xFC) & (~0x03);
                Delay1ms(1000);
            }
            
            /* 给出500HZ的方波驱动 */
            for(j = 0; j < 1000; j++)
            {
                /* P2.0----蜂鸣器 */
                P2 = (P2 & 0xFE) & (~0x01);
                Delay1ms(1);
                P2 = (P2 & 0xFE) | 0x01;
                Delay1ms(1);
            }
    
        }
         
    }
    
    
    
    
    /* end  file */

    这里需要注意的是,MT254X蓝牙4.0开发板使用的无源蜂鸣器,那么我们需要产生一个方波来驱动。如这代码:

            /* 给出500HZ的方波驱动 */
            for(j = 0; j < 1000; j++)
            {
                /* P2.0----蜂鸣器 */
                P2 = (P2 & 0xFE) & (~0x01);
                Delay1ms(1);
                P2 = (P2 & 0xFE) | 0x01;
                Delay1ms(1);
            }

    如果是有缘蜂鸣器,则没有那么麻烦,直接给出低电平驱动。为什么是低电平,我们看下原理图:

     使用的PNP三极管,并且使用续流二极管保护蜂鸣器。

    控制LED视频下载地址:

    http://pan.baidu.com/share/link?shareid=3562495290&uk=3996269986#dir/path=%2F%E6%9C%B1%E5%85%86%E7%A5%BAForARM%2F%E9%A6%92%E5%A4%B4%E7%A7%91%E6%8A%80%E8%93%9D%E7%89%994.0%E7%B3%BB%E5%88%97%2F%E8%93%9D%E7%89%994.0%E8%AF%BE%E7%A8%8B






  • 第五节  LCD12864显示

        上一节我们成功控制了LED和蜂鸣器,这一节我们马不停蹄接着LCD12864的控制。关于LCD12864的手册可以在网盘下载:

    http://pan.baidu.com/share/link?shareid=3562495290&uk=3996269986#dir/path=%2F%E6%9C%B1%E5%85%86%E7%A5%BAForARM%2F%E9%A6%92%E5%A4%B4%E7%A7%91%E6%8A%80%E8%93%9D%E7%89%994.0%E7%B3%BB%E5%88%97%2FMT254xBoard%E8%93%9D%E7%89%99%E5%BC%80%E5%8F%91%E6%9D%BF


        为了系统能够稳定的工作,首先我们将系统时钟切换到32M的外部晶振,为了自由配置所需要的时钟,主要借助于CLKCONCMD.OSC选择系统主时钟,而借助于CLKCONCMD.OSC32K则用于选择芯片32K时钟源!而低功耗模式设置时,需要借助于SLEEPCMD寄存器,在《CC253x-CC2540-41Applications User's Guide.pdf》中并没有说明SLEEPCMD第二位功能,如下所示:

        但是参考cc2430芯片的说明书可以发现,对应的SLEEP寄存器则有说明,如下所示,这个是TI有意隐藏芯片细节,当SLEEPCMD.OSC_PD为0时,32MHz晶振与16MHz RC振荡器都会起振:

        对于SLEEPSTA寄存器中BIT6/BIT5说明在cc2530说明书中也并没有说明,可以参考cc2430说明书中内容,其中第6位XOSC_STB表明外部高速32M晶振是否上电并稳定起振,当稳定时该位为1;同样对于第5位HFRC_STB则表明内部16MHz高速RC振荡器是否起振,并是否稳定,当16MHz RC振荡器稳定时该位为1。

    void SysStartXOSC(void)
    {
        SLEEPCMD &= ~0x04;                      // 启动所有晶振
        while (!(SLEEPSTA & 0x40));             // 等待晶振稳定
    
        CLKCONCMD = (CLKCONCMD & 0x80) | 0x49;  // 使用16M晶振作为主时钟
        while ((CLKCONSTA & ~0x80) != 0x49 );   // 等待主时钟切换到16M晶振
    
        CLKCONCMD = (CLKCONCMD & ~0x80) ;       // 使用外部32K晶振作为休眠时钟
        while ( (CLKCONSTA & 0x80) != 0 );      // 等待睡眠时钟切换到外部32K晶振
    
        CLKCONCMD = (CLKCONCMD & 0x80) ;        // 使用32M晶振作为主时钟
        while ( (CLKCONSTA & ~0x80) != 0 );     // 等待主时钟切换到32M晶振
    
        SLEEPCMD |= 0x04;                       // 关闭未使用的晶振
    }

     按照上述方式配置后,我们就可以工作在外部的32M晶振上了,配置好系统时钟和SPI后,剩下的工作只需要按照液晶屏的说明书发送相应的指令就可以将液晶屏驱动起来了,具体的驱动代码详见下一堂课程。这里使用的是ASCII的点阵表,所以只能显示英文,如果需要显示中文,就需要中文字库的支持了。
       
    LCD12864的驱动程序:

    /******************************************************************************
    
                      版权所有 (C), 2013-2020, 深圳市馒头科技有限公司
    
     ******************************************************************************
      文 件 名   : Lcd12864.c
      版 本 号   : V1.0
      作    者   : 朱兆祺
      生成日期   : 2014年6月18日
      功能描述   : LCD12864驱动
                  //control
                  P0.1 - LCD_MODE
                  P1.2 - LCD_CS
    
                  //spi
                  P1.5 - CLK
                  P1.6 - MOSI
      函数列表   :
      修改历史   :
      1.日    期   : 2014年6月18日
        作    者   : 朱兆祺
        修改内容   : 创建文件
    
    ******************************************************************************/
    
    /*----------------------------------------------*
     * 包含头文件                                   *
     *----------------------------------------------*/
    #include <ioCC2540.h>
    #include "Lcd12864.h"
    #include "common.h"
    
    /*----------------------------------------------*
     * 宏定义                                       *
     *----------------------------------------------*/
    
    /* LCD lines */
    #define LCD12864_MAX_LINE                64
    #define LCD12864_MAX_ROW                 128
    
    #define HAL_LCD_FONT_LINES                8
    #define HAL_LCD_FONT_ROWS                 6
    
    /* LCD Max Chars and Buffer */
    #define HAL_LCD_MAX_LINES            (LCD12864_MAX_LINE/HAL_LCD_FONT_LINES)       // 6*8点阵最大行数
    #define HAL_LCD_MAX_CHARS            (LCD12864_MAX_ROW/HAL_LCD_FONT_ROWS)         // 6*8点阵最大列数
    
    /* LCD Control lines */
    #define HAL_LCD_RS_PORT             0
    #define HAL_LCD_RS_PIN              1
    
    #define HAL_LCD_CS_PORT             1
    #define HAL_LCD_CS_PIN              2
    
    /* LCD SPI lines */
    #define HAL_LCD_CLK_PORT            1
    #define HAL_LCD_CLK_PIN             5
    
    #define HAL_LCD_MOSI_PORT           1
    #define HAL_LCD_MOSI_PIN            6
    
    // 12864 命令
    #define                LCD_CMD_DISPLAY_ON                                0xAF
    #define                LCD_CMD_DISPLAY_OFF                                0xAE
    #define                LCD_CMD_BEGIN_LINE                                0x40
    #define                LCD_CMD_PAGE_LINE                                0xB0
    #define                LCD_CMD_ROW_HIG                                        0x10
    #define                LCD_CMD_ROW_LOW                                        0x00
    #define                LCD_CMD_READ_STATE                                0x00
    #define                LCD_CMD_ROW_ADDR_NORMAL                        0xA0                // 从左到右
    #define                LCD_CMD_ROW_ADDR_REVERSE                 0xA1                // 从右到左
    #define                LCD_CMD_DISPLAY_NORMAL                        0xA6
    #define                LCD_CMD_DISPLAY_REVERSE                        0xA7
    #define                LCD_CMD_DISPLAY_POINT_ALL                0xA5
    #define                LCD_CMD_DISPLAY_POINT_NORMAL        0xA4
    #define                LCD_CMD_BIAS_SET                                0xA2                // 0XA2:BIAS=1/9 (常用)  0XA3:BIAS=1/7
    #define                LCD_CMD_SOFT_RESET                                0xE2
    #define                LCD_CMD_LINE_NORMAL                                0xC0                // 从上到下
    #define                LCD_CMD_LINE_REVERSE                        0xC8                // 从下到上
    #define                LCD_CMD_POWER_ONE                                0x2C
    #define                LCD_CMD_POWER_TWO                                0x2E
    #define                LCD_CMD_POWER_THREE                                0x2F
    #define                LCD_CMD_CONTRAST_ONE_LEVEL                0x22  // 0x20-0x27
    #define                LCD_CMD_CONTRAST_TWO_CMD                0x81  // 0x00-0x3F
    #define                LCD_CMD_STATIC_PICTURE_ON                0xAD
    
       /* SPI interface control */
    #define LCD_SPI_BEGIN()     HAL_CONFIG_IO_OUTPUT(HAL_LCD_CS_PORT,  HAL_LCD_CS_PIN,  0); /* chip select */
    #define LCD_SPI_END()                                                         \
    {                                                                             \
      asm("NOP");                                                                 \
      asm("NOP");                                                                 \
      asm("NOP");                                                                 \
      asm("NOP");                                                                 \
      HAL_CONFIG_IO_OUTPUT(HAL_LCD_CS_PORT,  HAL_LCD_CS_PIN,  1); /* chip select */         \
    }
    /* clear the received and transmit byte status, write tx data to buffer, wait till transmit done */
    #define LCD_SPI_TX(x)                   { U1CSR &= ~(BV(2) | BV(1)); U1DBUF = x; while( !(U1CSR & BV(1)) ); }
    
    /* Control macros */
    #define LCD_DO_WRITE()        HAL_CONFIG_IO_OUTPUT(HAL_LCD_RS_PORT,  HAL_LCD_RS_PIN,  1);
    #define LCD_DO_CONTROL()      HAL_CONFIG_IO_OUTPUT(HAL_LCD_RS_PORT,  HAL_LCD_RS_PIN,  0);
    
    /*全体ASCII 列表:5x7点阵库*/
    const static uint8 aucAsciiTable5x7[][5]={
    0x00,0x00,0x00,0x00,0x00,//space
    0x00,0x00,0x4f,0x00,0x00,//!
    0x00,0x07,0x00,0x07,0x00,//"
    0x14,0x7f,0x14,0x7f,0x14,//#
    0x24,0x2a,0x7f,0x2a,0x12,//$
    0x23,0x13,0x08,0x64,0x62,//%
    0x36,0x49,0x55,0x22,0x50,//&
    0x00,0x05,0x07,0x00,0x00,//]
    0x00,0x1c,0x22,0x41,0x00,//(
    0x00,0x41,0x22,0x1c,0x00,//)
    0x14,0x08,0x3e,0x08,0x14,//*
    0x08,0x08,0x3e,0x08,0x08,//+
    0x00,0x50,0x30,0x00,0x00,//,
    0x08,0x08,0x08,0x08,0x08,//-
    0x00,0x60,0x60,0x00,0x00,//.
    0x20,0x10,0x08,0x04,0x02,///
    0x3e,0x51,0x49,0x45,0x3e,//0
    0x00,0x42,0x7f,0x40,0x00,//1
    0x42,0x61,0x51,0x49,0x46,//2
    0x21,0x41,0x45,0x4b,0x31,//3
    0x18,0x14,0x12,0x7f,0x10,//4
    0x27,0x45,0x45,0x45,0x39,//5
    0x3c,0x4a,0x49,0x49,0x30,//6
    0x01,0x71,0x09,0x05,0x03,//7
    0x36,0x49,0x49,0x49,0x36,//8
    0x06,0x49,0x49,0x29,0x1e,//9
    0x00,0x36,0x36,0x00,0x00,//:
    0x00,0x56,0x36,0x00,0x00,//;
    0x08,0x14,0x22,0x41,0x00,//<
    0x14,0x14,0x14,0x14,0x14,//=
    0x00,0x41,0x22,0x14,0x08,//>
    0x02,0x01,0x51,0x09,0x06,//?
    0x32,0x49,0x79,0x41,0x3e,//@
    0x7e,0x11,0x11,0x11,0x7e,//A
    0x7f,0x49,0x49,0x49,0x36,//B
    0x3e,0x41,0x41,0x41,0x22,//C
    0x7f,0x41,0x41,0x22,0x1c,//D
    0x7f,0x49,0x49,0x49,0x41,//E
    0x7f,0x09,0x09,0x09,0x01,//F
    0x3e,0x41,0x49,0x49,0x7a,//G
    0x7f,0x08,0x08,0x08,0x7f,//H
    0x00,0x41,0x7f,0x41,0x00,//I
    0x20,0x40,0x41,0x3f,0x01,//J
    0x7f,0x08,0x14,0x22,0x41,//K
    0x7f,0x40,0x40,0x40,0x40,//L
    0x7f,0x02,0x0c,0x02,0x7f,//M
    0x7f,0x04,0x08,0x10,0x7f,//N
    0x3e,0x41,0x41,0x41,0x3e,//O
    0x7f,0x09,0x09,0x09,0x06,//P
    0x3e,0x41,0x51,0x21,0x5e,//Q
    0x7f,0x09,0x19,0x29,0x46,//R
    0x46,0x49,0x49,0x49,0x31,//S
    0x01,0x01,0x7f,0x01,0x01,//T
    0x3f,0x40,0x40,0x40,0x3f,//U
    0x1f,0x20,0x40,0x20,0x1f,//V
    0x3f,0x40,0x38,0x40,0x3f,//W
    0x63,0x14,0x08,0x14,0x63,//X
    0x07,0x08,0x70,0x08,0x07,//Y
    0x61,0x51,0x49,0x45,0x43,//Z
    0x00,0x7f,0x41,0x41,0x00,//[
    0x02,0x04,0x08,0x10,0x20,// 斜杠
    0x00,0x41,0x41,0x7f,0x00,//]
    0x04,0x02,0x01,0x02,0x04,//^
    0x40,0x40,0x40,0x40,0x40,//_
    0x01,0x02,0x04,0x00,0x00,//`
    0x20,0x54,0x54,0x54,0x78,//a
    0x7f,0x48,0x48,0x48,0x30,//b
    0x38,0x44,0x44,0x44,0x44,//c
    0x30,0x48,0x48,0x48,0x7f,//d
    0x38,0x54,0x54,0x54,0x58,//e
    0x00,0x08,0x7e,0x09,0x02,//f
    0x48,0x54,0x54,0x54,0x3c,//g
    0x7f,0x08,0x08,0x08,0x70,//h
    0x00,0x00,0x7a,0x00,0x00,//i
    0x20,0x40,0x40,0x3d,0x00,//j
    0x7f,0x20,0x28,0x44,0x00,//k
    0x00,0x41,0x7f,0x40,0x00,//l
    0x7c,0x04,0x38,0x04,0x7c,//m
    0x7c,0x08,0x04,0x04,0x78,//n
    0x38,0x44,0x44,0x44,0x38,//o
    0x7c,0x14,0x14,0x14,0x08,//p
    0x08,0x14,0x14,0x14,0x7c,//q
    0x7c,0x08,0x04,0x04,0x08,//r
    0x48,0x54,0x54,0x54,0x24,//s
    0x04,0x04,0x3f,0x44,0x24,//t
    0x3c,0x40,0x40,0x40,0x3c,//u
    0x1c,0x20,0x40,0x20,0x1c,//v
    0x3c,0x40,0x30,0x40,0x3c,//w
    0x44,0x28,0x10,0x28,0x44,//x
    0x04,0x48,0x30,0x08,0x04,//y
    0x44,0x64,0x54,0x4c,0x44,//z
    0x08,0x36,0x41,0x41,0x00,//{
    0x00,0x00,0x77,0x00,0x00,//|
    0x00,0x41,0x41,0x36,0x08,//}
    0x04,0x02,0x02,0x02,0x01,//~
    };
    const uint8 asciiTableSize = sizeof( aucAsciiTable5x7 ) / sizeof( aucAsciiTable5x7[0]);
    
    
    /*****************************************************************************
     函 数 名  : LCD12864_Cmd
     功能描述  : 发送控制命令
     输入参数  : uint8 cmd
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年5月28日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    static void LCD12864_Cmd(uint8 cmd)
    {
        LCD_SPI_BEGIN();
        LCD_DO_CONTROL();
        LCD_SPI_TX(cmd);
        LCD_SPI_END();
    }
    
    /*****************************************************************************
     函 数 名  : LCD12864_Dat
     功能描述  : 发送数据
     输入参数  : uint8 data
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年5月28日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    static void LCD12864_Dat(uint8 data)
    {
        LCD_SPI_BEGIN();
        LCD_DO_WRITE();
        LCD_SPI_TX(data);
        LCD_SPI_END();
    }
    
    
    void LCD12864_Init(void)
    {
        PERCFG |= 0x02;       // 设置UART alt2 为 SPI
        // 配置引脚为SPI功能
        HAL_CONFIG_IO_PERIPHERAL(HAL_LCD_CLK_PORT,  HAL_LCD_CLK_PIN);
        HAL_CONFIG_IO_PERIPHERAL(HAL_LCD_MOSI_PORT, HAL_LCD_MOSI_PIN);
    
        /* Configure SPI */
        U1UCR  = 0x80;      // 清除原来的数据
        U1CSR  = 0x00;      // SPI 主机模式
        // 高位在前,第一个上升沿发送数据,波特率为2M
        U1GCR  = HAL_SPI_TRANSFER_MSB_FIRST | HAL_SPI_CLOCK_PHA_0 | HAL_SPI_CLOCK_POL_LO | 0x0F;
        U1BAUD = 0xFF;
    
        // CS RS 配置为输出
        HAL_CONFIG_IO_OUTPUT(HAL_LCD_RS_PORT, HAL_LCD_RS_PIN, 1);
        HAL_CONFIG_IO_OUTPUT(HAL_LCD_CS_PORT, HAL_LCD_CS_PIN, 1);
    
        SoftWaitUs(15000); // 15 ms
        LCD12864_Cmd(LCD_CMD_SOFT_RESET);        //软复位
            SoftWaitUs(15000); // 15 ms
            LCD12864_Cmd(LCD_CMD_POWER_ONE);        //升压步聚1
            SoftWaitUs(15); // 15 us
            LCD12864_Cmd(LCD_CMD_POWER_TWO);        //升压步聚2
            SoftWaitUs(15); // 15 us
            LCD12864_Cmd(LCD_CMD_POWER_THREE);        //升压步聚3
            SoftWaitUs(150); // 15 us
            LCD12864_Cmd(LCD_CMD_CONTRAST_ONE_LEVEL);        //粗调对比度,可设置范围0x20~0x27
            LCD12864_Cmd(LCD_CMD_CONTRAST_TWO_CMD);        //微调对比度
            LCD12864_Cmd(0x3a);        //0x1a,微调对比度的值,可设置范围0x00~0x3f
            LCD12864_Cmd(LCD_CMD_BIAS_SET);        // 1/9偏压比(bias)
            LCD12864_Cmd(LCD_CMD_LINE_NORMAL);        //行扫描顺序:从上到下
            LCD12864_Cmd(LCD_CMD_ROW_ADDR_REVERSE);        //列扫描顺序:从左到右
            LCD12864_Cmd(LCD_CMD_BEGIN_LINE);        //起始行:第一行开始
            LCD12864_Cmd(LCD_CMD_DISPLAY_ON);        //打开显示
        LCD12864_Cmd(LCD_CMD_DISPLAY_POINT_NORMAL);
        LCD12864_Cmd(LCD_CMD_DISPLAY_NORMAL);       //设置为正显模式
            SoftWaitUs(150); // 150 us
    }
    
    
    /*****************************************************************************
     函 数 名  : LCD12864_SetAddr
     功能描述  : 设置起始地址
     输入参数  : uint8 line
                 uint8 col
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年6月1日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    static void LCD12864_SetAddr(uint8 line, uint8 col)
    {
        uint8 ucLine, ucRow;
        //line += 5;
        col  += 4;
        if((line >= LCD12864_MAX_LINE) || (col >= LCD12864_MAX_ROW))
        {
            return;
        }
    
        ucLine = LCD_CMD_PAGE_LINE | (line&0x0f);
        LCD12864_Cmd(ucLine);
        SoftWaitUs(15);
    
        ucRow = LCD_CMD_ROW_HIG | (col>>4);
        LCD12864_Cmd(ucRow);
        SoftWaitUs(15); // 15 us
    
        ucRow = LCD_CMD_ROW_LOW | (col&0x0f);
        LCD12864_Cmd(ucRow);
        SoftWaitUs(15); // 15 us
    }
    
    /*****************************************************************************
     函 数 名  : LCD12864_Dis5X8
     功能描述  : 将一个字符用5*8的点阵显示
     输入参数  : char ch
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年6月1日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    static void LCD12864_Dis5X8(char ch)
    {
        uint8 ucCnt;
        if((ch >= 0x20)&&(ch < 0x7f))
        {
            uint8 ucChar = ch - 0x20;
            for(ucCnt=0; ucCnt<5; ucCnt++)
            {
                LCD12864_Dat( aucAsciiTable5x7[ucChar][ucCnt]);
            }
            //LCD12864_Dat(0x00);
        }
        else if(ch==0x00)     //不需要显示,清空指定位置
        {
                    for(ucCnt=0; ucCnt<5; ucCnt++)
            {
                LCD12864_Dat(0x00);
            }
            }
        LCD12864_Dat(0x00);
    }
    
    
    /*****************************************************************************
     函 数 名  : LCD12864_Clear
     功能描述  : 清屏
     输入参数  : void
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年6月1日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    void LCD12864_Clear(void)
    {
        uint8 ucLine, ucRow;
        for(ucLine=0; ucLine<LCD12864_MAX_LINE; ucLine++)
        {
            LCD12864_SetAddr(ucLine, 0);
            for(ucRow=0; ucRow<LCD12864_MAX_ROW; ucRow++)
            {
                    LCD12864_Dat(0x00);
            }
        }
    }
    
    /*****************************************************************************
     函 数 名  : LCD12864_DisChar
     功能描述  : 在指定位置显示一个字符
     输入参数  : uint8 line
                 uint8 col
                 char ch
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年6月1日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    void LCD12864_DisChar(uint8 line, uint8 col, char ch)
    {
        if (( line < HAL_LCD_MAX_LINES)&&(col < HAL_LCD_MAX_CHARS))
        {
            LCD12864_SetAddr(line, col*HAL_LCD_FONT_ROWS);
            LCD12864_Dis5X8(ch);
        }
    }
    
    /*****************************************************************************
     函 数 名  : LCD12864_DisStr
     功能描述  : 将字符串显示到指定行
     输入参数  : uint8 line         显示的行 0~7
                 char* pStr         显示的字符串首地址
     输出参数  : 无
     返 回 值  :
    
     修改历史      :
      1.日    期   : 2014年6月2日
        作    者   : 朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    void LCD12864_DisStr(uint8 line, char* pStr)
    {
        uint8 ucCnt = 0;
        for ( ucCnt = 0 ; ucCnt < HAL_LCD_MAX_CHARS; ucCnt++ )
        {
            if ( '\0' == *pStr )
            {
                break;
            }
            LCD12864_DisChar( line, ucCnt, pStr[ucCnt]);
        }
    
        for (  ; ucCnt < HAL_LCD_MAX_CHARS; ucCnt++ )
        {
            LCD12864_DisChar( line, ucCnt, 0);
        }
    }
    
    /*----------------------------------------------*
     *              end of file                     *
     *----------------------------------------------*/

    主程序:

    /*****************************************************************************
     函 数 名  : main
     功能描述  : 主函数
     输入参数  : 无
     输出参数  : 无
     返 回 值  : 
    
     修改历史      :
      1.日    期   : 2014年6月6日
        作    者   :  朱兆祺
        修改内容   : 创建
    
    *****************************************************************************/
    
    int main(void)
    {
        /* 启动外部晶振 */
        SysStartXOSC();
        
        /* LCD12864的初始化 */
        LCD12864_Init();
        
        /* 清屏 */
        LCD12864_Clear();
        
        
        while(1)
        {
            /* 显示字符 */
            LCD12864_DisStr(3, "ShenZhenShiManTouKeJi");
        }
        
        return 0;
    }

    这样我们就点亮的LCD12864屏幕:




  • 很给力,支持一下

  • 第六节  独立按键之查询方式

        在MT254xboard上有一个独立按键KEY1,如图 ,独立按键和复位键在整个班子的左上角。按键通过P0.0口和CPU连接,在没有按键时为高电平,按下后为低电平。下面我们通过LCD来显示独立按键的状态。

         其对应的原理图如下:

         我们先用查询的方式读取按键的状态。因为按键接入在P0.0口,所以我们读取P0.0口的电平即可知道按键的状态。

    uint8 KeyValue(void)                                // 读取按键状态
    {
        if((P0&0X01) == 0X00 )      // 按下为低电平
        {
            return KEY_DOWN;
        }
        else
        {
            return KEY_UP;
        }
    }

        这里我们在while循环中不断的读取按键状态,并且判断是否改变,如果改变则改变LCD的显示。

    int main(void)
    {
        uint8 OldKeyValue = 0;                
        uint8 NewKeyValue = 0;
        SysStartXOSC();
        LCD12864_Init();
        LCD12864_DisStr(1, "    Key Test");
            // 按键初始化
        P0SEL &= ~0X01;        // 设置为 IO功能
        P0DIR &= ~0X01;        // 设置为输入功能
    
        while(1)
        {
            NewKeyValue = KeyValue();   // 读取按键状态
            if(OldKeyValue != NewKeyValue)  // 按键状态改变
            {
                OldKeyValue = NewKeyValue;  // 保存当前按键状态
                if(OldKeyValue == KEY_DOWN)
                {
                    LCD12864_DisStr(3, "    Key Down ");
                }
                else
                {
                    LCD12864_DisStr(3, "    Key Up ");
                }
            }
        }
        return 0;
    }

        运行程序,效果如图所示:



  • 第七节  独立按键之中断方式

         复制Key工程,重命名为KeyInterrupt。刚刚我们用查询的方式读取按键的状态。但是这种方式在实际的工程中没有实际的应用价值,下面我们采用外部中断的方式来读取按键的状态,每当按键按下时就会触发一次外部中断。为了P0.0口能够触发中断,我们需要进行如下配置:

        P0IEN |= 0X01;  // P00 设置为中断方式
        PICTL &=~ 0X01; // 下降沿触发
        IEN1 |= 0X20;   // 允许P0口中断
        P0IFG = 0x00;   // 清除中断标志位
        EA = 1;         // 开总中断

        然后就需要编写中断服务函数了。这里注意一点,在IAR中的中断函数有点特殊,格式为:

    #pragma vector = 中断向量
    __interrupt 函数

        所以我们的中断函数为:

    #pragma vector = P0INT_VECTOR
    __interrupt void P0_ISR(void)
    {
        if(0x01&P0IFG)
        {
            NewKeyValue = KEY_DOWN;     // 记录按键按下
        }
        P0IFG = 0;              //清中断标志
        P0IF = 0;               //清中断标志
    }

        在中断中我们记录按键按下,等待应用程序处理。而在主函数中我们需要处理按键按下事件,主函数中我们对按键计数并且通过LCD显示。

    int main(void)
    {
        char LCDBuf[21]={0};            // 显存
        int KeyCnt = 0;
        SysStartXOSC();
        LCD12864_Init();
        LCD12864_DisStr(1, "    Key Test");
    
        P0SEL &= ~0X01; // 设置为IO功能
        P0DIR &= ~0X01; // 设置为输入功能
        P0IEN |= 0X01;  // P0.0 设置为中断方式
        PICTL |= 0X01;  // 下降沿触发
        IEN1 |= 0X20;   // 允许P0口中断
        P0IFG = 0x00;   // 清除中断标志位
        EA = 1;         // 开总中断
    
        sprintf(LCDBuf, "   Key Count : %d", KeyCnt++);    // 按键计数
        LCD12864_DisStr(3, LCDBuf);
        while(1)
        {
            if(KEY_DOWN == NewKeyValue)  // 按键按下
            {
                SoftWaitUs(25000);       // 延时防抖
                if((P0&0X01) == 0X00)   // 再次确认按键是否按下
                {
                    sprintf(LCDBuf, "   Key Count : %d", KeyCnt++);    // 按键计数
                    LCD12864_DisStr(3, LCDBuf);
                }
                else
                {
                    NewKeyValue = KEY_UP;   // 按键松开
                }
            }
        }
        return 0;
    }

        每按一次按键计数加1,效果如图所示:





  • 第八节  CC254x内部温度传感器温度采集

        CC254x内部有一个温度传感器,我们这节使用这个传感器来采集芯片的温度,此传感器精度不高。不适合用于实际的工程中,这里只为演示AD采样。要使用内部的温度采集我们需要使用AD采样,所以我们需要先来了解CC254x的AD功能。在后续课程有对ADC的详细说明。

        ADC结构图如下所示:

        ADC控制寄存器1如下图所示:

        我们使用手动触发的方式进行AD采样,所以STSEL = 11B,最低两位始终为1,最终ADCCON1=0x33。

        ADC控制寄存器3如图所示:

        ADC参考电压使用内部电压,采用12位精度采集。采集温度通道。所以ADCCON3= 0x3E。这里注意一点,ADCCON2和ADCCON3的配置是一样的,我们这里用ADCCON3来配置。

    uint16 ADC_Read (uint8 channel)
    {
        int16  reading = 0;
        uint8  adcChannel = 0x01<<channel;
        int16  Result = 0;
    
        if (channel <= 7)   // 通道0-7需要通过P0.0-P0.7输入
        {
            ADCCFG |= adcChannel;
        }
    
        uint8 i=0;
        do{
            ADCCON3 = channel | 0x20;           // 12位精度,启动转换
            while (!(ADCCON1 & 0x80));          // 等待转换完成
    
            // 读取采样结果
            reading = (int16)(ADCL);
            reading |= (int16)(ADCH << 8);
            reading >>= 4;                      // 丢弃低位
            Result += reading;                  // 累加
        }while(i++ < 10);   // 连续采样10次
    
        if (channel <= 7)
        {
            ADCCFG &= (adcChannel ^ 0xFF);
        }
    
        return (Result/10);
    }

        在读取温度值前,我们还需要使能温度传感器。

    int main(void)
    {
        float temp=0;
        char LCDBuf[21] = {0};
        SysStartXOSC();         // 启动外部晶振
        LCD12864_Init();        // LCD初始化
        // 打开温度传感器
        TR0 = 0x01;
        ATEST = 0x01;
    
        while(1)
        {
            temp = (ADC_Read(TEMP_ADC_CHANNEL) - 1340) /10.0;
            sprintf(LCDBuf, "   temp : %0.1f", temp); //
            LCD12864_DisStr(3, LCDBuf);
            SoftWaitUs(100000);
        }
        return 0;
    }

        采集的温度显示在LCD上,可以看到温度在跳动,这是由于AD的误差太大导致的,这里只做一个简单的实验,如果需要工程应用,建议外接温度传感器。把手放在芯片上可以看到温度在上升。温度采集结果如下图所示:





  • 第九节  五向按键

        五向按键,也就是我们平常所见的摇杆内部构造,五向按键有上下左右和中间五个按键值,MT254xboard上的五向按键检测电路由馒头科技自主设计,而不是Ti的设计,采用一个外部中断和一个AD检测口来完成按键的检测。

        由原理图可知当我们按下不同的键值时在JOY_CHK将会产生一个上升沿,并且在JOY_AD口有不同的电压。我们只需要在JOY_CHK的外部中断中读取JOY_AD的电压即可识别不同的按键。

        外部中断和AD采用在前面已经讲过了,这里只需要拿来用就可以了。JOY_CHK连接在P0.7脚,JOY_AD连接在P0.6脚。我们将按键值显示在LCD上。

    int main(void)
    {
        uint8 KeyValue = 0;
    
        SysStartXOSC();
        LCD12864_Init();
        LCD12864_DisStr(1, "    JoyStick Test");
    
        P0INP |= 0X40;  // P0.6 三态
        P0SEL &= ~0X80; // 设置为IO功能
        P0DIR &= ~0X80; // 设置为输入功能
        P0IEN |= 0X80;  // P0.7 设置为中断方式
        PICTL &= ~0X80;  // 上升沿触发
        IEN1 |= 0X20;   // 允许P0口中断
        P0IFG = 0x00;   // 清除中断标志位
        EA = 1;         // 开总中断
    
        while(1)
        {
            if(KeyStat)  // 按键按下
            {
                KeyValue = GetKeyValue();
                switch ( KeyValue )
                {
                    case KEY_UP :
                        sprintf(LCDBuf, "\tUP");
                        break;
    
                    case KEY_DOWN :
                        sprintf(LCDBuf, "\tDown");
                        break;
    
                    case KEY_LEFT :
                        sprintf(LCDBuf, "\tLeft");
                        break;
    
                    case KEY_CENTER :
                        sprintf(LCDBuf, "\tCenter");
                        break;
    
                    case KEY_RIGHT :
                        sprintf(LCDBuf, "\tRight");
                        break;
    
                    default:
                        break;
                }
                KeyStat =0;
                LCD12864_DisStr(3, LCDBuf);
            }
        }
        return 0;
    }

        按键的检测通过电压来区分。

    uint8 GetKeyValue(void)
    {
        uint16 adc;
        uint8 ksave0 = 0;
    
        adc = ADC_Read (JOY_AD_CHANNEL);
    
        if ((adc >= 800) && (adc <= 1100))
        {
            ksave0 = KEY_RIGHT;
        }
        else if ((adc >= 1200) && (adc <= 2000))
        {
            ksave0 = KEY_CENTER;
        }
        else if ((adc >= 2050) && (adc <= 2150))
        {
            ksave0 = KEY_UP;
        }
        else if ((adc >= 2200) && (adc <= 2230))
        {
            ksave0 = KEY_LEFT;
        }
        else if ((adc >= 2240) && (adc <= 2500))
        {
            ksave0 = KEY_DOWN;
        }
        return ksave0;
    }

        使用五向按键效果如下所示:




  • 第十节  蜂鸣器

        蜂鸣器是一种常用的报警设备,常用的蜂鸣器有无源和有源两种类型,无源蜂鸣器需要用一定频率的方波驱动,从而发出不同频率的声音。而有源蜂鸣器只需要通电就会发出固定频率的声音,MT254xboard开发板上的蜂鸣器用的是无源蜂鸣器,因此我们需要用一定频率的方波来驱动。

        硬件驱动方面,我们这里使用了PNP三极管来驱动蜂鸣器,BUZZ引脚为芯片的P2.0。对照IO复用表可知,此IO可以作为定时器4的匹配通道1输出。所以我们需要把定时器配置为PWM匹配输出模式:

     PERCFG |= (0x01<<4);   // 选择定时器4匹配功能中的第2种IO口
    
        P2DIR |= 0x01;          // p2.0  输出
        P2SEL |= 0x01;          // p2.0  复用功能
    
        T4CTL &= ~0x10;             // Stop timer 3 (if it was running)
        T4CTL |= 0x04;              // Clear timer 3
        T4CTL &= ~0x08;             // Disable Timer 3 overflow interrupts
        T4CTL |= 0x03;              // Timer 3 mode = 3 - Up/Down
    
        T4CCTL0 &= ~0x40;           // Disable channel 0 interrupts
        T4CCTL0 |= 0x04;            // Ch0 mode = compare
        T4CCTL0 |= 0x10;            // Ch0 output compare mode = toggle on compare

        这里仅仅是配置为匹配输出,具体输出什么样的波形还需要我们再通过计算得出。

    void Buzzer_Start(uint16 frequency)
    {
        P2SEL |= 0x01;          // p2.0  复用功能
        uint8 prescaler = 0;
        // Get current Timer tick divisor setting
        uint8 tickSpdDiv = (CLKCONSTA & 0x38)>>3;
    
        // Check if frequency too low
        if (frequency < (244 >> tickSpdDiv)){   // 244 Hz = 32MHz / 256 (8bit counter) / 4 (up/down counter and toggle on compare) / 128 (max timer prescaler)
            Buzzer_Stop();                       // A lower tick speed will lower this number accordingly.
        }
    
        // Calculate nr of ticks required to achieve target frequency
        uint32 ticks = (8000000/frequency) >> tickSpdDiv;      // 8000000 = 32M / 4;
        // Fit this into an 8bit counter using the timer prescaler
        while ((ticks & 0xFFFFFF00) != 0)
        {
            ticks >>= 1;
            prescaler += 32;
        }
    
        // Update registers
        T4CTL &= ~0xE0;
        T4CTL |= prescaler;
        T4CC0 = (uint8)ticks;
        // Start timer
        T4CTL |= 0x10;
    }

         这个函数是通过传入参数的形式,使P2.0口发出指定频率的方波。

    void Buzzer_Stop(void)
    {
        T4CTL &= ~0x10;             // Stop timer 3
        P2SEL &= ~0x01;
        P2_0 = 1;
    }

        这个函数是使蜂鸣器停止,主要有三个动作,停止定时器,将P2.0配置为IO功能并且输出高电平,因为我们使用的是PNP三极管。

        我们在按键的程序上加上蜂鸣器的控制,当按下按键时,蜂鸣器响。松开后停止响。

    int main(void)
    {
        char LCDBuf[21]={0};            // 显存
        int KeyCnt = 0;
    
        SysStartXOSC();
        LCD12864_Init();
        LCD12864_DisStr(1, "    Buzzer Test");
        Buzzer_Init();
    
        P0SEL &= ~0X01; // 设置为IO功能
        P0DIR &= ~0X01; // 设置为输入功能
        P0IEN |= 0X01;  // P0.0 设置为中断方式
        PICTL |= 0X01;  // 下降沿触发
        IEN1 |= 0X20;   // 允许P0口中断
        P0IFG = 0x00;   // 清除中断标志位
        EA = 1;         // 开总中断
    
        sprintf(LCDBuf, "   Key Count : %d", KeyCnt++);    // 按键计数
        LCD12864_DisStr(3, LCDBuf);
        while(1)
        {
            if(KEY_DOWN == NewKeyValue)  // 按键按下
            {
                SoftWaitUs(25000);       // 延时防抖
                if((P0&0X01) == 0X00)   // 再次确认按键是否按下
                {
                    sprintf(LCDBuf, "   Key Count : %d", KeyCnt++);    // 按键计数
                    LCD12864_DisStr(3, "    Buzzer Start");
                    Buzzer_Start(2000);
                }
                else
                {
                    NewKeyValue = KEY_UP;   // 按键松开
                    Buzzer_Stop();
                    LCD12864_DisStr(3, "    Buzzer Stop");
                }
            }
        }
        return 0;
    }

        按下按键后可以看到LCD显示Buzzer Start,听到蜂鸣器响,如果你有示波器,还能测到P2.0口有一个2KHz的方波。





  • 第十一节  串口通信

        在软件开发过程中调试是一个很关键的过程,而调试用的最多的手段就是打印Log,嵌入式平台很少有显示设备,所以我们需要将信息通过串口打印到PC端。

        MT254xboard上已经通过RS232芯片将UART0连接到DB9,我们只需要将DB9连接到电脑即可,UART0 对应的外部设备 IO 引脚关系为:P0_2------RX,P0_3------TX。

        我们需要将这两个IO配置为复用功能,CC2540的USART可以配置为SPI模式或者异步UART模式,这里我们需要配置为异步UART模式。

        首先配置IO为UART模式:

        PERCFG &= ~0x01;        // 配置UART为位置 1
        P0SEL = 0x3c;           // P0_2,P0_3,P0_4,P0_5用作串口功能
        P2DIR &= ~0XC0;         // P0 优先作为UART0

         配置UART0寄存器,将UART0配置为8N1模式,波特率为115200。

        U0CSR |= 0x80;          // UART 方式
        U0GCR |= 11;            // U0GCR与U0BAUD配合
        U0BAUD |= 216;          // 波特率设为115200
        UTX0IF = 0;             // 清除中断标志
        U0CSR |= 0X40;          // 允许接收
        IEN0 |= 0x84;           // 开总中断,接收中断

         这里采用中断方式来接收串口数据,并在中断中回调应用层的接收处理函数。

    #pragma vector = URX0_VECTOR
    __interrupt void UART0_ISR(void)
    {
        uint8 ch;
        URX0IF = 0;    // 清中断标志
        ch = U0DBUF;
        if ( NULL != RecvCb )   // 调用回调函数
        {
            RecvCb(ch);
        }
    }

         为了测试串口的通讯功能,这里我们通过串口接收命令的方式来控制LED的亮灭和蜂鸣器的响和停止,并且显示当前的状态。根据串口输出提示,发送对应字符可以实现相应功能,并且显示状态。




  • 第十二节  Flash的读写

        嵌入式系统中需要存储数据,而片内的Flash资源很匮乏,所以我们经常需要使用SpiFlash来存储数据,MT254xboard中板载了一个512Kbyte的Flash,下面我们来驱动此Flash。上一小节中我们用SPI的方式驱动了LCD12864,这节我们继续用SPI来驱动板载的Flash,《GD25Q40.pdf》详细的说明了如何驱动这片Flash,在此不做累述,我们复制LCD12864工程,重命名为SpiFlash,在此工程中添加GD25Q40的两个驱动文件。

        下面我们来检测这个Flash,检测的方法为,全部写入0xAA,然后再读出,对比是否为0xAA,如果是,那Flash是没有问题的,否则Flash可能已经有坏块。具体的代码见例程,这个过程所需要的时间取决于我们需要检测的区域大小,如果完全检测,则可能需要几分钟的时间。

    int main(void)
    {
        SysStartXOSC();
    
    
        LCD12864_Init();        // LCD初始化
        GD25Q40_Init();         // Flash初始化
    
        LCD12864_DisStr(0, "Flash Check....");
        sprintf(LCDBuf, "Flash ID :%04X", GD25Q40_ReadID());    // 读取器件ID
        LCD12864_DisStr(1, LCDBuf);
        GD25Q40_EraseChip();        // 擦除整片Flash 大约需要10S
        LCD12864_DisStr(2, "Erase Chip Complete");
        uint32 iCnt = 0;
    
        // 全部写入0xAA
        const uint8 Write = 0xAA;
        for(iCnt=0; iCnt < CHECK_ADDR_RANGE; iCnt++)
        {
            GD25Q40_Write(&Write, iCnt, 1);     // 写入0xAA
        }
    
        // 读取Flash内部的值,与写入的值对比
        uint8 Read;
        for(iCnt=0; iCnt < CHECK_ADDR_RANGE; iCnt++)
        {
            GD25Q40_Read(&Read, iCnt, 1);
            if(Read != Write)
            {
                LCD12864_DisStr(3, "Flash Error");
                break;
            }
        }
        // 写入的值与读出的值完全一样
        if(iCnt >= CHECK_ADDR_RANGE)
        {
            LCD12864_DisStr(3, "Flash Check Success");
        }
        GD25Q40_EraseChip();    // 再次擦除
    
        while(1);
        return 0;
    }

    MT254X蓝牙4.0开发板Flash效果:

  • 通常只能达到11bit !

  • 第十三节   BLE协议栈简介

        TI的协议栈分为两部分:控制器和主机。对于4.0以前的蓝牙,这两部分是分开的。所有profile和应用都建构在GAP或GATT之上。根据这张图,我们从底层开始介绍。TI的这款CC2540器件可以单芯片实现BLE蓝牙协议栈结构图的所有组件,包括应用程序。

    1.1.1  PHY层

        1Mbps自适应跳频GFSK(高斯频移键控),运行在免证的2.4GHz。

    1.1.2  LL层

        LL层为RF控制器,控制设备处于准备(standby)、广播、监听/扫描(scan)、初始化、连接,这五种状态中一种。五种状态切换描述为:未连接时,设备广播信息,另外一个设备一直监听或按需扫描,两个设备连接初始化,设备连接上了。发起聊天的设备为主设备,接受聊天的设备为从设备,同一次聊天只能有一个意见领袖,即主设备和从设备不能切换。

    1.1.3   HCI层

        HCI层为接口层,向上为主机提供软件应用程序接口(API),对外为外部硬件控制接口,可以通过串口、SPI、USB来实现设备控制。

    1.1.4   L2CAP层

        L2CAP层提供数据封装服务,允许逻辑上的点对点通讯。

    1.1.5   SM层

        SM层提供配对和密匙分发,实现安全连接和数据交换。

    1.1.6   ATT层

        ATT层负责数据检索,允许设备向另外一个设备展示一块特定的数据称之为属性,在ATT环境中,展示属性的设备称之为服务器,与它配对的设备称之为客户端。链路层的主机从机和这里的服务器、客服端是两种概念,主设备既可以是服务器,也可以是客户端。从设备毅然。

    1.1.7   GATT层

        GATT层定义了使用 ATT 的服务框架和配置文件(profiles)的结构。BLE 中所有的数据通信都需要经过 GATT。GATT负责处理向上与应用打交道,其关键工作是把为检索工作提供合适的profile结构,而profile由检索关键词(characteristics)组成。

    1.1.8   GAP层

        GAP直接与应用程序或配置文件(profiles)通信的接口,处理设备发现和连接相关服务。另外还处理安全特性的初始化。对上级,提供应用程序接口,对下级,管理各级职能部门,尤其是指示LL层控制室五种状态切换,指导保卫处做好机要工作。

    1.2      TI协议栈源码介绍

        在第二章我们讲解了源码的安装,这里我们就来剖析源码的结构。打开协议栈目录我们可以看到下图:

    BLE源码:

      

    目录名

      

    内容

    说明

    Accessories

    一些工具和已经编译好的Hex文件

    此文件夹中有Btool的安装包、USB-CDC的驱动。

    Components

    Hal驱动,OSAL源码、协议栈通用源码

    此文件夹是OSAL各层组件的实现

    Documents

    帮助文档

    协议栈说明文档,这是学习BLE最好的资料。

    Projects

    工程文件

    这里有一些TI的Demo,我们开发一般是在Demo的基础上进行


        这里TI给出了很多Demo,这些例程都是经过了SIG评审的,ble 文件夹中有很多工程文件,有些是具体的应用,例如BloodPressure、GlucoseCollector 、GlucoseSensor 、 HeartRate 、HIDEmuKbd 等都为传感器的实际应用,有相应标准的 Profile。

        其中有4种角色: SimpleBLEBroadcaster 、 SimpleBLECentral 、SimpleBLEObserver、SimpleBLEPeripheral。

    他们都有自己的特点。

    1.Broadcaster 广播员 —— 非连接性的信号装置

    2.Observer 观察者 —— 扫描得到,但不能链接

    3.Peripheral 从机   —— 可链接,在单个链路层链接中作为从机

    4.Central 主机   —— 扫描设备并发起链接,在单链路层或多链路层中作为主机。

        我们的讲解将围绕这主机和从机进行。因为其它的设备都是基于这两种设备扩展开来的。


  • 第十四节 OSAL工作原理

    蓝牙为了实现同多个设备相连,或实现多功能,也实现了功能扩充,这就产生了调度问题。因为,虽然软件和协议栈可扩充,但终究最底层的执行部门只有一个。为了实现多事件和多任务切换,需要把事件和任务对应的应用,并起一个名字OSAL操作系统抽象层。


    OSAL管理的实现

        如果实现软件和硬件的低耦合,使软件不经改动或很少改动即可应用在另外的硬件上,这样就方便硬件改造、升级、迁移后,软件的移植。HAL硬件抽象层正是用来抽象各种硬件的资源,告知给软件。其作用类似于嵌入式系统设备驱动的定义硬件资源的h头文件。

    BLE低功耗蓝牙系统架构:

    OSAL作为调度核心,BLE协议栈、profile定义、所有的应用都围绕它来实现。OSAL不是传统大家使用的操作系统,而是一个允许软件建立和执行事件的循环。

    软件功能是由任务事件来实现的,创建一个任务事件需要以下工作:

    1. 创建task identifier任务ID;

    2. 编写任务初始化(task initialization routine)进程,并需要添加到OSAL初始化进程中,这就是说系统启动后不能动态添加功能;

    3. 编写任务处理程序;

    4. 如有需要提供消息服务。


        BLE协议栈的各层都是以OSAL任务方式实现,由于LL控制室的时间要求最为迫切,所以其任务优先级最高。为了实现任务管理,OSAL通过消息处理(messageprocess),存储管理,计时器定时等附加服务实现。


    系统启动流程:

    为了使用OSAL,在main函数的最后要启动一个名叫osal_start_system的进程,该进程会调用由特定应用决定的启动函数osalInitTasks(来启动系统)。osalInitTasks逐个调用BLE协议栈各层的启动进程来初始化协议栈。随后,设置一个任务的8bit任务ID(task ID),跳入循环等待执行任务,系统启动完成。


    1. 任务优先级决定于任务ID,任务ID越小,优先级越高

    2. BLE协议栈各层的任务优先级比应用程序的高

    3. 初始化协议栈后,越早调入的任务,任务ID越高,优先级越低,即系统倾向于处理新到的任务



        每个事件任务由对应的16bit事件变量来标示,事件状态由旗号(taskflag)来标示。如果事件处理程序已经完成,但其旗号并没有移除,OSAL会认为事情还没有完成而继续在该程序中不返回。比如,在SimpleBLEPeripheral实例工程中,当事件START_DEVICE_EVT发生,其处理函数SimpleBLEPeripheral_ProcessEvent就运行,结束后返回16bit事件变量,并清除旗语SBP_START_DEVICE_EVT。



        每当OSAL事件检测到了有任务事件,其相应的处理进程将被添加到由处理进程指针构成的事件处理表单中,该表单名叫taskArr(taskarray)。taskArr中各个事件进程的顺序和osalInitTasks初始化函数中任务ID的顺序是对应的。


        有两种,最简单的方法是使用osal_set_event函数(函数原型在OSAL.h文件中),在这个函数中,用户可以像定义函数参数一样设置任务ID和事件旗语。第二种方法是使用osal_start_timerEx函数(函数原型在OSAL_Timers.h文件中),使用方法同osal_set_event函数,而第三个以毫秒为单位的参数osal_start_timerEx则指示该事件处理必须要在这个限定时间内,通过定时器来为事件处理计时。

        类似于Linux嵌入式系统内存分配C函数mem_alloc,OSAL利用osal_mem_alloc提供基本的存储管理,但osal_mem_alloc只有一个用于定义byte数的参数。对应的内存释放函数为osal_mem_free。


        不同的子系统通过OSAL的消息机制通信。消息即为数据,数据种类和长度都不限定。消息收发过程描述如下:

    接收信息,调用函数osal_msg_allocate创建消息占用内存空间(已经包含了osal_mem_alloc函数功能),需要为该函数指定空间大小,该函数返回内存空间地址指针,利用该指针就可把所需数据拷贝到该空间。

        发送数据,调用函数osal_msg_send,需为该函数指定发送目标任务,OSAL通过旗语SYS_EVENT_MSG告知目标任务,目标任务的处理函数调用osal_msg_receive来接收发来的数据。建议每个OSAL任务都有一个消息处理函数,每当任务收到一个消息后,通过消息的种类来确定需要本任务做相应处理。消息接收并处理完成,调用函数osal_msg_deallocate来释放内存(已经包含了osal_mem_free函数功能)。

        为了实现更好的移植性,协议栈将硬件层抽象出了一个HAL硬件抽象层,当新的硬件平台做好后,只需修改HAL,而不需修改HAL之上的协议栈的其他组件和应用程序。




  • 第十五节 BLE蓝牙4.0协议栈启动分析

        TI的这款CC2540/CC2541器件可以单芯片实现BLE蓝牙协议栈结构图的所有组件,包括应用程序。从这章开始我们来剖析协议栈源码,我们选用SimpleBLEPeripheral工程开刀,这是一个从机的例程,基本的工作是对外广播,等待主机来连接,读写展示的属性。

        首先打开工程文件,打开后可以看到整个工程的结构。

        我们按照系统的启动顺序来一步一步走,我们都知道在C代码中,一般启动的首个函数为main,这个函数在SimpleBLEPeripheral_Main.c中,打开文件,可以看到这个文件只有一个main函数和一个函数的申明,我们暂时不理会那个申明的函数,先看main都做了些什么工作:

    Int  main(void)
    {
      /* Initialize hardware */
      HAL_BOARD_INIT();          // 硬件初始化
    
      // Initialize board I/O
      InitBoard( OB_COLD );         // 板级初始化
    
      /* Initialze the HAL driver */
      HalDriverInit();              // Hal驱动初始化
    
      /* Initialize NV system */
      osal_snv_init();              // Flash存储SNV初始化
    
      /* Initialize LL */
    
      /* Initialize the operating system */
      osal_init_system();           // OSAL初始化
    
      /* Enable interrupts */
      HAL_ENABLE_INTERRUPTS();      // 使能总中断
    
      // Final board initialization
      InitBoard( OB_READY );        // 板级初始化
    
      #if defined ( POWER_SAVING )
        osal_pwrmgr_device( PWRMGR_BATTERY );   // 低功耗管理
      #endif
    
      /* Start OSAL */
      osal_start_system(); // No Return from here  启动OSAL
    
      return 0;
    }

        通过代码我们可以看到,系统启动的过程,主要是做了一些初始化,如果开启了低功耗,则还需要开启低功耗管理。我们先不去理会初始化做了什么,但是我们知道在main函数的最后启动了OSAL,那么我们就进去看看OSAL是如何运作的。

        在IAR中如果需要跳转到某个函数或变量的定义,可以在此函数名中右击然后选择Go To Definition……就可以调到相应的定义。

    void osal_start_system( void )
    {
    #if !defined ( ZBIT ) && !defined ( UBIT )
      for(;;)  // Forever Loop
    #endif
      {
        osal_run_system();
      }
    }

        这里看到我们进入了一个死循环,并且一直调用osal_run_system(),那我们再进入此函数。

    </blockquote></div><div style="text-align: left;"><div class="blockcode"><blockquote>void osal_run_system( void )
    {
      uint8 idx = 0;
    
    #ifndef HAL_BOARD_CC2538
      osalTimeUpdate();     // 定时器更新
    #endif
    
      Hal_ProcessPoll();    // Hal层信息处理
    
      do {
        if (tasksEvents[idx])  // Task is highest priority that is ready.
        {
          break;
        }
      } while (++idx < tasksCnt);   // 检查每个人任务是否有事件
    
      if (idx < tasksCnt)   // 有事件发生
      {
        uint16 events;
        halIntState_t intState;
    
        HAL_ENTER_CRITICAL_SECTION(intState);   // 进入临界区
        events = tasksEvents[idx];
        tasksEvents[idx] = 0;  // Clear the Events for this task. 清除事件标志
        HAL_EXIT_CRITICAL_SECTION(intState);    // 退出临界区
    
        activeTaskID = idx;
        events = (tasksArr[idx])( idx, events );    // 执行事件处理函数
        activeTaskID = TASK_NO_TASK;
    
        HAL_ENTER_CRITICAL_SECTION(intState);   // 进入临界区
        tasksEvents[idx] |= events;  // Add back unprocessed events to the current task.
        HAL_EXIT_CRITICAL_SECTION(intState);    // 退出临界区
      }
    #if defined( POWER_SAVING )         // 没有事件发生,并且开启了低功耗模式
      else  // Complete pass through all task events with no activity?
      { // 系统进入低功耗模式
        osal_pwrmgr_powerconserve();  // Put the processor/system into sleep
      }
    #endif
    
      /* Yield in case cooperative scheduling is being used. */
    #if defined (configUSE_PREEMPTION) && (configUSE_PREEMPTION == 0)
      {
        osal_task_yield();
      }
    #endif
    }

        在这里可以看到这个OSAL的核心,整个OSAL通过检测每个任务是否有事件发生,如果有则执行相应的任务,处理相应的事件。如果没有事件需要处理并且开启了低功耗模式,则系统就会进入低功耗模式。

        这里有一个很关键的地方,OSAL是如何知道哪个事件需要哪个任务来处理呢?

    events = (tasksArr[idx])( idx, events );    // 执行事件处理函数

        我们看这里有一个很关键的数组tasksArr,很显然,这是一个函数指针数组,我们看看它的定义。

    const pTaskEventHandlerFn tasksArr[] =
    {
      LL_ProcessEvent,                                                  // task 0
      Hal_ProcessEvent,                                                 // task 1
      HCI_ProcessEvent,                                                 // task 2
    #if defined ( OSAL_CBTIMER_NUM_TASKS )
      OSAL_CBTIMER_PROCESS_EVENT( osal_CbTimerProcessEvent ),       // task 3
    #endif
      L2CAP_ProcessEvent,                                               // task 4
      GAP_ProcessEvent,                                                 // task 5
      GATT_ProcessEvent,                                                // task 6
      SM_ProcessEvent,                                                  // task 7
      GAPRole_ProcessEvent,                                             // task 8
      GAPBondMgr_ProcessEvent,                                         // task 9
      GATTServApp_ProcessEvent,                                         // task 10
      SimpleBLEPeripheral_ProcessEvent                                    // task 11
    };

        可以看到在这个数组的定义中,每个成员都是任务的执行函数,按照任务的优先级排序,并且在osalInitTasks中初始化的时候,我们可以看到每个任务都有一个对应的初始化函数,并且传递了一个taskID,此ID从0开始自增,这里有一点非常重要,初始化的顺序和任务数组的定义顺序是一样的,这就保证了我们给任务发生消息或事件时能够准确的传递到相应的任务处理函数。

    void osalInitTasks( void )
    {
      uint8 taskID = 0;
    
      tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
      osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
    
      /* LL Task */
      LL_Init( taskID++ );
    
      /* Hal Task */
      Hal_Init( taskID++ );
    
      /* HCI Task */
      HCI_Init( taskID++ );
    
    #if defined ( OSAL_CBTIMER_NUM_TASKS )
      /* Callback Timer Tasks */
      osal_CbTimerInit( taskID );
      taskID += OSAL_CBTIMER_NUM_TASKS;
    #endif
    
      /* L2CAP Task */
      L2CAP_Init( taskID++ );
    
      /* GAP Task */
      GAP_Init( taskID++ );
    
      /* GATT Task */
      GATT_Init( taskID++ );
    
      /* SM Task */
      SM_Init( taskID++ );
    
      /* Profiles */
      GAPRole_Init( taskID++ );
      GAPBondMgr_Init( taskID++ );
    
      GATTServApp_Init( taskID++ );
    
      /* Application */
      SimpleBLEPeripheral_Init( taskID );
    }

        应用层的初始化SimpleBLEPeripheral_Init,SimpleBLEPeripheral_Init( uint8task_id )主要对 GAP 和 GATT 进行配置,最后调用osal_set_event(simpleBLEPeripheral_TaskID, SBP_START_DEVICE_EVT )启动设备。

        设备启动后应用层就能接收到这个设置的事件并进行处理,可以看到设备启动中主要是启动设备,注册绑定管理,并且启动了一个定时器,这个定时器是一个周期事件的第一次启动。

        周期事件中每次都会重启这个定时器,并且处理周期事件。

        在初始化的时候我们注册了一个很重要的函数,设备状态改变时的回调函数,这个函数在设备的状态改变时会被底层的协议栈回调,我们可以从这个回调函数中看的设备的状态的改变。

        static void peripheralStateNotificationCB( gaprole_States_t newState);

        从函数的定义可以看出,设备的状态类型都在数据类型gaprole_States_t中定义了,我们看一下这个数据类型的定义:

    typedef enum
    {
      GAPROLE_INIT = 0,                       //!< Waiting to be started
      GAPROLE_STARTED,                        //!< Started but not advertising
      GAPROLE_ADVERTISING,                    //!< Currently Advertising
      GAPROLE_WAITING,                        //!< Device is started but not advertising, is in waiting period before advertising again
      GAPROLE_WAITING_AFTER_TIMEOUT,          //!< Device just timed out from a connection but is not yet advertising, is in waiting period before advertising again
      GAPROLE_CONNECTED,                      //!< In a connection
      GAPROLE_CONNECTED_ADV,                  //!< In a connection + advertising
      GAPROLE_ERROR                           //!< Error occurred - invalid state
    } gaprole_States_t;

         看到这个定义就很明确了,设备的状态就在这几种状态间切换。







  • 第十六节 协议栈LED实验

        TI的协议栈中在HAL层已经有了LED的驱动,我们只需要针对我们的开发板进行配置即可,我们的开发板有两个LED,分别对应P1.0和P1.1。这个在裸机开发的时候已经介绍了。

        为了保持协议栈原有的代码不变,我们在BLE-CC254x-1.4.0\Components\hal\target目录下新建一个文件夹,使它适应我们的开发板。

        打开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\r\n", osal_strlen("Hello MT254xBoard\r\n"));
    }

        在串口回调函数中我们只做一件事,将串口接收到的数据显示到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中。


  • 第二十节   DHT11温湿度传感器

    DHT11简介

        DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性和卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高等优点。每个DHT11传感器都在极为精确的湿度校验室中进行校准。校准系数以程序的形式存在OTP内存中,传感器内部在检测型号的处理过程中要调用这些校准系数。单线制串行接口,使系统集成变得简易快捷。超小的体积、极低的功耗,使其成为给类应用甚至最为苛刻的应用场合的最佳选择。产品为4针单排引脚封装,连接方便。

    技术参数

    Ø 供电电压: 3.3~5.5V DC

    Ø 输 出: 单总线数字信号

    Ø 测量范围: 湿度20-90%RH, 温度0~50℃

    Ø 测量精度: 湿度+-5%RH, 温度+-2℃

    Ø 分 辨 率: 湿度1%RH,温度1℃

    Ø 互 换 性: 可完全互换 ,

    Ø 长期稳定性: <±1%RH/年

        DHT11 数字湿温度传感器采用单总线数据格式。即,单个数据引脚端口完成输入输出双向传输。其数据包由 5Byte(40Bit)组成。数据分小数部分和整数部分,一次完整的数据传输为40bit,高位先出。DHT11 的数据格式为:8bit 湿度整数数据+8bit 湿度小数数据+8bit 温度整数数据+8bit 温度小数数据+8bit 校验和。其中校验和数据为前四个字节相加。传感器数据输出的是未编码的二进制数据。数据(湿度、温度、整数、小数)之间应该分开处理。例如,某次从 DHT11 读到的数据如图所示:

    协议栈DHT11测试

        打开DHT11Example工程,我们在启动事件中对DHT11进行初始化。如果初始化失败则说明没有接传感器。


        然后在定时事件中定时的读取温湿度的值。并将结果通过UART显示到PC端。


        从其中可以看到当前的温度为29摄氏度,湿度为30%,往传感器器哈一口气可以看到温湿度都上升了。



  • 第二十一节 蓝牙协议栈之从机通讯

        之前都是外围模块的驱动程序,这一节开始,我们进入蓝牙4.0协议栈的核心部分,从机通讯的程序设计。接下来的章节是蓝牙4.0协议栈最为核心的程序设计部分。

        前面的大都是外围器件的实验,这节我们介绍蓝牙通讯中从机的角色,从机的主要工作是对外广播,接受主机的连接,并且接受主机发送过来的数据。这里介绍两个函数:

    bStatus_t GAPRole_SetParameter( uint16 param, uint8 len, void *pValue );

            这个函数主要是用来配置从机的一些参数,第一个参数表示需要配置哪个参数,例如我们需要时能从机广播,则需要这样调用:

        uint8 initial_advertising_enable = TRUE;
    GAPRole_SetParameter( GAPROLE_ADVERT_ENABLED, sizeof( uint8 ), &initial_advertising_enable );

            第二个函数是特征值改变时的回调函数,当主机给从机发送数据时,从机就会回调这个函数来告知应用层有数据送达。

    static void simpleProfileChangeCB( uint8 paramID );

        在低功耗蓝牙中,数据的传输是通过特征值的读写来实现的。

        BLE协议栈的GATT层用于应用程序在两个连接设备之间的数据通信的。从GATT层的角度看,当设备连接后,将充当一下两种角色中的一个:

        GATT Client  —— 从GATT服务器读/写数据的设备。

        GATT Server —— 包含客户端需要读/写的数据的设备。

        重要的是要注意,GATTClient和Server 的角色完全独立于BLE的链路层的 slave和master的角色,或GAP层peripheral和central的角色。一个slave 可以是GATT Client或GATT Server,一个master同样可以是GATT Client或GATT Server。 一个GATT Server可以有多个完成一个特定的功能或特性GATT Server组成。

    在SimpleBLEPeripheral应用程序中有三个GATT服务:

        Mandatory GAP Service:这个服务包含设备和访问信息,比如设备名称、供应商和产品标识。

        Mandatory GATT Service :这个服务包含有关服务UUID相关信息。

    SimpleGATTProfile Service——这个服务是一个示例配置文件,供测试和演示。

    Profile简介

        为了更容易的保持Bluetooth 设备之间的兼容,Bluetooth规范中定义了 Profile。Profile 定义了设备如何实现一种连接或者应用,你可以把 Profile 理解为连接层或者应用层协议。Bluetooth 的一个很重要特性,就是所有的 Bluetooth 产品都无须实现全部的 Bluetooth 规范,你可根据所需要的产品实现需要的Profile,不必给开发带来更大的开销。这就是说当需要利用蓝牙提供数据传输功能时就必须建立对应的Profile,TI的BLE协议栈为我们提供了部分Profile,其中一部分是非标准的Profile。其中非标准的有SimpleGATTProfile和SimpleKeysProfile,我们将通过对这两个Profile的介绍及实验来了解Profile的特性和使用。每个 Profile 初始化其响应的服务和内部寄存器。GATT 服务器将整个服务加到属性表中,并为每个属性分配唯一的句柄。 GATTProfile用于存储和处理GATT服务器中的数据。在下面的实验中需要用到的都是我们自己新建的Profile,即非标准的Profile。其中主要要注意Profile、UUID、handle、CharacteristicValues。


    SimpleGATTProfile及Btool的使用

    SimpleGATTProfile中包含5个特征值,每一个的属性都不同:

        SimpleGATTProfile 特征值属性:

         Btool是PC端工具,使用特定的HCI命令与CC2540通信,PC端需要通过串口或 USB 连接 CC2540,CC2540 使用 HostTestRelease 工程,硬件可以使用 USBDongle(对应CC2540USB)或我们提供的USBDongle。


    USBDongle连接从机

        使用馒头科技有限公司的USBDongle,烧写HostTestRelease固件,连接电脑后就可以用Btool软件来连接从机设备。

        将从机工程编译下载到开发板,连接串口到PC端,我们通过串口来观察设备的运行,运行后可以看到设备处于广播。

       这是我们插入USBDongle到电脑,可以看到识别到一个串口插入,如图,这就是USBDongle用CDC的方式实现的串口。

            打开Btool,按左图配置,可以看到右图的信息,这是说明Btool已经识别到了USBDongle。

    Btool的界面可以分为4个区:

    1. 设备信息展示

    2. 历史记录

    3. 设备控制

    4. 连接信息

        确保周围存在设备可发现,点击Discover/Connect标签的scan按钮,CC2540 就会进行10s的扫描过程,在这期间可通过Cancle按钮停止扫描。

        可以看到,我们周边有两个设备,其中一个就是我们的开发板,根据串口输出的信息我们知道我们设备的地址是0X7C669D9F6297,下面我们点击establish来连接我们的开发板。

    连接后可以看到两边都同时显示了连接信息。

        开发板输出连接:

    Btool连接的设备信息:

    特征值的读写

        接下来我们用Btool对SimpleProfile 进行使用操作。刚刚我们已经列出了SimpleProfile中的各个特征值。


        使用UUID读取特征值,CHAR1具有读写属性,这里对 SimpleProfile 的第一特征值 CHAR1进行读取操作,UUID 为0xfff1。选择 Read/Write 选项页并选择 ReadUsing Characteristic UUID 功能,在Characteristic UUID选项填入f1:ff(高字节在前),点击Read按钮。

        读取特征值成功:

        下面对此特征值进行写入操作,写入操作必须使用Handle值进行,而无法使用UUID来操作,那CHAR1的Handle值的什么呢?其实刚刚在我们读取CHAR1的值的时候就已经获取到了它的Handle。如图,CHAR1的Handle为0x0025。

        CHAR1的Handle值:

       下面我们通过这个Handle对CHAR1写入十进制的10,如图,我们写入成功了。

        写入成功:


    在SimpleBLEPeripheral设备的串口输出中可以看到设备提示CHAR1的值变为了10。

        下面来验证我们是否成功的将CHAR1改为了10,按照刚刚读取CHAR1的步骤,重新读取CHAR1的值。

        CHAR1的值改为了10:

    蓝牙点灯

        上面我们已经能够成功的改写一个特征值,那我们是不是可以通过发送特定的值来控制一个灯的亮灭呢?答案是肯定的。下面我们来实现这个功能。


        从机工程已经有5个特征值了,我们现在增加一个特征值来控制灯的亮灭。那我们该如何来添加特征值呢?特征值的管理是在profile中实现的。所以我们需要对profile进行修改。

    (1)修改simpleGATTProfile.h

        在simpleGATTProfile.h中可以看到现在定义的5个特征值的标示符和UUID,我们添加一个1Byte的特征值来控制灯的亮灭

        因为simpleGATTProfile是共用的文件,为了不影响其它工程,我们使用一个宏来控制新增加的属性。

        接下来我们需要修改simpleGATTProfile.c,这个文件需要修改的地方较多,下面我们一步一步来修改。

    (2)添加UUID

    (3)添加属性

    (4)属性表

    (5)属性设置操作

    (6)属性获取操作

    (7)属性读操作

    (8)属性写操作

         Profile的改造完成后,我们将这个宏打开,配置工程。

        接着我们在staticvoid simpleProfileChangeCB( uint8 paramID )函数的switch中加入CHAR6的判断即可。

        编译烧录后,按照我们前面说的在Btool中对FFF6的UUID进行读写操作即可实现对LED的控制。











  • 第二十二节 蓝牙协议栈主从一体之主机通讯

        随着蓝牙4.0模块的大量使用,为了很多从未接触过蓝牙的工程师也能快速便捷地开发蓝牙项目或者使用蓝牙,主从一体、远控IO等等特性也成为蓝牙模块必备的条件。其实,联合第二十一节和本节(第二十二节),我们就能将一个本无固件的裸片蓝牙,使其开发为具备主从一体功能的蓝牙模块。这两节的内容,也是本连载篇的重点部分之一。

        上一节我们对从机的工作流程有了一个整体的把握。我们现在接着来看主机的工作流程。

        主机的工作主要是扫描设备,对发现的设备发起连接,然后就是对特征值的读写操作了。


    手动连接

        从机的对外广播是在初始化的时候完成的,那主机的扫描是在哪里开始的呢?阅读源码可以发现主机的操作都在按键处理中完成的。主机通过五向按键中的五个按键实现不同的功能。

    static void simpleBLECentral_HandleKeys( uint8 shift, uint8 keys )
    {
      (void)shift;  // Intentionally unreferenced parameter
    
      if ( keys & HAL_KEY_UP )      // 向上
      {
        // Start or stop discovery
        if ( simpleBLEState != BLE_STATE_CONNECTED )    // 如果没有连接,开始扫描
        {
          if ( !simpleBLEScanning )
          {
            simpleBLEScanning = TRUE;
            simpleBLEScanRes = 0;
    
            LCD_WRITE_STRING( "Discovering...", HAL_LCD_LINE_1 );
            LCD_WRITE_STRING( "", HAL_LCD_LINE_2 );
    
            GAPCentralRole_StartDiscovery( DEFAULT_DISCOVERY_MODE,
                                           DEFAULT_DISCOVERY_ACTIVE_SCAN,
                                           DEFAULT_DISCOVERY_WHITE_LIST );
          }
          else
          {
            GAPCentralRole_CancelDiscovery();
          }
        }
        else if ( simpleBLEState == BLE_STATE_CONNECTED && // 如果连接并且发现Handle进行读写操作
                  simpleBLECharHdl != 0 &&
                  simpleBLEProcedureInProgress == FALSE )
        {
          uint8 status;
    
          // Do a read or write as long as no other read or write is in progress
          if ( simpleBLEDoWrite )
          {
            // Do a write
            attWriteReq_t req;
    
            req.handle = simpleBLECharHdl;
            req.len = 1;
            req.value[0] = simpleBLECharVal;
            req.sig = 0;
            req.cmd = 0;
            status = GATT_WriteCharValue( simpleBLEConnHandle, &req, simpleBLETaskId );
          }
          else
          {
            // Do a read
            attReadReq_t req;
    
            req.handle = simpleBLECharHdl;
            status = GATT_ReadCharValue( simpleBLEConnHandle, &req, simpleBLETaskId );
          }
    
          if ( status == SUCCESS )
          {
            simpleBLEProcedureInProgress = TRUE;
            simpleBLEDoWrite = !simpleBLEDoWrite;
          }
        }
      }
    
      if ( keys & HAL_KEY_LEFT )    // 左
      {
        // Display discovery results
        if ( !simpleBLEScanning && simpleBLEScanRes > 0 )   // 显示扫描到的设备
        {
            // Increment index of current result (with wraparound)
            simpleBLEScanIdx++;
            if ( simpleBLEScanIdx >= simpleBLEScanRes )
            {
              simpleBLEScanIdx = 0;
            }
    
            LCD_WRITE_STRING_VALUE( "Device", simpleBLEScanIdx + 1,
                                    10, HAL_LCD_LINE_1 );
            LCD_WRITE_STRING( bdAddr2Str( simpleBLEDevList[simpleBLEScanIdx].addr ),
                              HAL_LCD_LINE_2 );
        }
      }
    
      if ( keys & HAL_KEY_RIGHT )   // 右
      {
        // Connection update
        if ( simpleBLEState == BLE_STATE_CONNECTED )    // 如果连接,则更新连接
        {
          GAPCentralRole_UpdateLink( simpleBLEConnHandle,
                                     DEFAULT_UPDATE_MIN_CONN_INTERVAL,
                                     DEFAULT_UPDATE_MAX_CONN_INTERVAL,
                                     DEFAULT_UPDATE_SLAVE_LATENCY,
                                     DEFAULT_UPDATE_CONN_TIMEOUT );
        }
      }
    
      if ( keys & HAL_KEY_CENTER )  // 中间键
      {
        uint8 addrType;
        uint8 *peerAddr;
    
        // Connect or disconnect
        if ( simpleBLEState == BLE_STATE_IDLE )     // 空闲则连接
        {
          // if there is a scan result
          if ( simpleBLEScanRes > 0 )
          {
            // connect to current device in scan result
            peerAddr = simpleBLEDevList[simpleBLEScanIdx].addr;
            addrType = simpleBLEDevList[simpleBLEScanIdx].addrType;
    
            simpleBLEState = BLE_STATE_CONNECTING;
    
            GAPCentralRole_EstablishLink( DEFAULT_LINK_HIGH_DUTY_CYCLE,
                                          DEFAULT_LINK_WHITE_LIST,
                                          addrType, peerAddr );
    
            LCD_WRITE_STRING( "Connecting", HAL_LCD_LINE_1 );
            LCD_WRITE_STRING( bdAddr2Str( peerAddr ), HAL_LCD_LINE_2 );
          }
        }
        else if ( simpleBLEState == BLE_STATE_CONNECTING ||     // 连接则断开连接
                  simpleBLEState == BLE_STATE_CONNECTED )
        {
          // disconnect
          simpleBLEState = BLE_STATE_DISCONNECTING;
    
          gStatus = GAPCentralRole_TerminateLink( simpleBLEConnHandle );
    
          LCD_WRITE_STRING( "Disconnecting", HAL_LCD_LINE_1 );
        }
      }
    
      if ( keys & HAL_KEY_DOWN )        // 下
      {
        // Start or cancel RSSI polling
        if ( simpleBLEState == BLE_STATE_CONNECTED )    // 连接则读取RSSi的值
        {
          if ( !simpleBLERssi )
          {
            simpleBLERssi = TRUE;
            GAPCentralRole_StartRssi( simpleBLEConnHandle, DEFAULT_RSSI_PERIOD );
          }
          else
          {
            simpleBLERssi = FALSE;
            GAPCentralRole_CancelRssi( simpleBLEConnHandle );
    
            LCD_WRITE_STRING( "RSSI Cancelled", HAL_LCD_LINE_1 );
          }
        }
      }
    }

        因为从机一直处于广播状态,所以秩序将上一节中的从机程序烧录进开发板即可,然后将主机程序烧录到另外一快开发板,通过五向按键来实现和从机的连接和读写功能。

    (1) 上电提示

        从机上电提示:

        主机上电提示:

    (2)根据主机的按键功能,我们按“UP”键,开始搜索周边设备。搜索完成后,可以看到,扫描到了一个设备。

        扫描到一个设备:

    (3)接着我们查看扫描到的设备地址,按左键。可以看到扫描到的设备地址为0x7C669D9F638A。这个地址正是我们的从机地址。

        显示从机地址:

    (4)按中间键连接从机,可以看到主机提示连接成功,从机也提示连接成功。
        主机提示连接成功:

    从机提示连接成功:

    (5)接着我们开始读取从机的RSSI值,按下键。

    (6)再次按下键,取消RSSI值的读取。

    (7)对从机的CHAR1进行读写,再次按上键读取到CHAR1的值为1。

    (8)接着按上键,对CHAR1写入0,同时看到从机提示CHAR1的值被修改为0。

        主机写入成功:

    从机提示CHAR1被改变:

    上电自动连接

        上一节中我们通过五向按键实现了主机连接从机的功能,这一节中们来实现主机上电后自动搜索连接从机。

        要实现连接,从机必须处于广播状态,剩下的工作全部由主机完成,扫描、发起连接。

        主机的状态也有回调函数,主机启动后,第一个状态是初始化,所以我们在初始化完成时开始扫描,

       这样开机后主机就会开始扫描周边设备,接下来我们在扫描完成后对扫描到的设备发起连接。

       将工程编译下载后通过串口助手观察主机和从机的输出可以发现主机上电后自动的完成了一系列的操作。


        主机提示:

       从机提示:




  • 第二十三节  OAD空中升级

    通过仿真器更新程序或者通过USB更新固件那都是一般人都可以实现的操作,但是要想实现OAD空中升级,这还是需要一定的技术能力。这一节我就带着大家完善这一能力。
        OAD:  on air download,指空中下载模式。当我们的产品发布以后,有时需要对固件进行升级,OAD是升级方式中的一种。

    配置BIM(Boot Image Manger)

        打开IAR,打开BLE-CC254x-1.4.0下的工程C:\TexasInstruments\BLE-CC254x-1.4.0\Projects\ble\util\BIM\cc254x\BIM.eww,然后编译,下载到开发板中。

    配置Image A

        打开OADExample工程,配置工程,添加如下几个宏

        FEATURE_OAD_BIM

        HAL_IMAGE_A

        FEATURE_OAD

        OAD_KEEP_NV_PAGES

    (1)  打开simplePeripheral.c找到宏定义DEFAULT_ENABLE_UPDATE_REQUEST,将其改为FLASE。否则会影响BLEDevice Monitor对其进行空中升级,到时可以再改回来。

    (2)  IAR左侧导航中找到Profile文件夹,点击右键添加oad_target.c和oad_target.h两个文件,二文件位于C:\TexasInstruments\BLE-CC254x-1.4.0\Projects\ble\Profiles\OAD中。

    (3)  在IAR导航中找到HAL→Target→MT254xboard→Driver,右键添加文件hal_crc.c,该文件位于C:\TexasInstruments\BLE-CC254x-1.4.0\Components\hal\target\MT254xboard中。

    (4)  在刚才的simplePeripheral.c文件中,找到simplePeripheral_Init()函数,在里面添加OADTarget_AddService()函数。

    (5)  在simplePeripheral.c的前面,添加引用OAD的头文件,OAD.h和OAD_target.h。

    (6)  点击Project→Option…,或直接按Alt+F7,选择BuildActions,在Post-buildcommand line中添加:

    "$PROJ_DIR$\..\..\common\cc2540\cc254x_ubl_pp.bat""$PROJ_DIR$" " rodUBL""$PROJ_DIR$\CC2540-OAD-ImgA\Exe\OADExample"

    注意,各双引号之间只有一个空格。注意图中红框标的部分,Image_A是和之前第1步对应的。

    (7)  点击Project→Option…,或直接按Alt+F7,选择Linker,选择Config,Linker Configurationfile中勾选Override default,添加C:\TexasInstruments\BLE-CC254x-1.4.0\Projects\ble\common\cc2540\cc254x_f256_imgA.xcl。

    (8)  点击Project→Option…,或直接按Alt+F7,选择Linker,选择Extra Output。

    (9)  Extra Option添加Hex文件的输出。

    (10)  点击OK,编译下载,如果出现如下错误,是因为我们使用的是IAR8051 8.30版本,如果你使用的是IAR8.20版本就没有这问题。

    (11)  这个问题是因为使用了虚拟寄存器导致的,我们找到如下文件。

    (12)  对文件的115行进行如下修改,将虚拟寄存器注释掉。

    (13)  编译后,可以看到我们生成的文件

    (14)  我们将hex文件叠加到BIM后面

      这样ImageA就成功烧录进开发板了。




    配置Image B

    (1)  方法如Image A,其它要注意的几个地方:点击Project→Option…,或直接按Alt+F7,选择C/C++Compiler,选择Preprocessor,将Defined symbols中的HAL_IMAGE_A改成B。

    (2)  点击Project→Option…,或直接按Alt+F7,选择Linker,选择Config,将Linker Configuration file中的文件改为B。

    (3)  保存后编译,同样的可以看到生成的文件。

    (4)  为了区分A和B镜像,我们将最终结果改名。



    空中升级

        有了bin文件就可以进行空中升级了,打开BLE Device Monitor(没有安装的需要安装),打开后软件会自动扫描设备,如图,我们扫描到了开发板。

    (2)  连接后,打开OAD

    (3)  点击file,选择Progame(OAD),选择生成的bin文件,可以看到我们当前运行的是A固件

    (4)  点击start,当进行到100%,就完成了固件空中升级。

    (5)  升级后再次打开OAD选项,可以看到选择运行的固件已经是B版本的了。

    注意:

    要把 BLE协议栈BLE-CC254x-1.4.0 安装在C盘,在其他盘符下没有生成bin文件。






  • 这一节不是昨天上午就有了么。。。

  • 这一节没有发过。

  • 第二十四节  SBL串口升级

        SBL升级和OAD升级的配置步骤都是一样的,主要是配置的参数不一样,下面我们来配置一个SBL升级的固件。

    配置SBL

        打开IAR,打开BLE-CC254x-1.4.0下的工程C:\TexasInstruments\BLE-CC254x-1.4.0\Projects\ble\util\SBL\iar\cc254x\sbl.eww,然后编译,下载到开发板中。


    配置Bin文件

    (1)  添加宏

        MAKE_CRC_SHDW

        FEATURE_SBL

        OAD_KEEP_NV_PAGES

    (2)  添加build选项

    "$PROJ_DIR$\..\..\common\cc2540\cc254x_ubl_pp.bat""$PROJ_DIR$" "ProdUBL" "$PROJ_DIR$\MT254xboard\Exe\SBLExample"

    (3)  Config选项

    (4)  Extra Output选项

    (5)  保存编译

    (6)  打开串口升级软件SerialBootTool.exe,选择SBLExample.bin文件。

    (7)  Load Image




  • 第二十五节 UBL-USB升级

     UBL升级是使用USB功能来进行升级的,它的配置也是一样的步骤。

    配置UBL

    (1)  打开IAR,打开BLE-CC254x-1.4.0下的工程C:\Texas Instruments\BLE-CC254x-1.4.0\Projects\ble\util\UBL\soc_8051\usb_msd\iar\ubl.eww,选择CC2540-Nano。

    (2)  这里有点改动,因为TI提供的UBL是使用P2.0上拉D+的,而我们的开发板是使用P0.4经过三极管上拉的,所以这里还需要修改上拉的引脚。

    (3)  编译下载,将开发板的用MiniUSB连接到电脑,就可以看到有一个U盘插入了。这里有一点要注意,在Win8的系统上好像识别这个U盘有问题,需要Win7一下的才支持。


    配置升级固件

    升级固件的配置和SBL几乎是一样的,主要有两处不一样的地方需要更改。

    (1)  添加宏

    OAD_KEEP_NV_PAGES

    FEATURE_UBL_MSD

    (2)  配置Config

    (3)  Build选项

    (4)  编译后可以看到

    (5)  固件升级

        升级的方法很简单,只需要将生成的Bin文件拷贝到U盘中即可。




  • 第二十六节  MTBeacon基站使用iPhone空中升级

    在iPhone端实现固件更新

        为了使用iPhone或者iPad对iBeacon进行固件更新,首先要确认手机为iPhone4S及以上,系统为IOS7及以上,在AppStore里搜索下载Multitool,这个APP是TI免费提供的,可以直接下载安装。安装好这个APP后,PC上需要使用iTunes将固件拷贝到手机。

    iTunes操作

        这里我们将需要更新的固件A和B都拷贝到手机。


    iPhone操作

    1. 打开Multitool,搜索设备

    2.  连接iBeacon

        这步要确保iBeacon没有进行部署,否则无法连接上设备,如果已经进行了临时部署,可以将电池卸下重新安装后才可以连接。连接后可以看到如下界面,选择Update FW。

    3. 选择升级模式

        选择高速模式,并且进入固件选择。

    4.  选择Shared File

        进入固件选择界面,这里注意,我们可以看到选择运行的固件为A固件,所以我们需要选择B固件进行更新(如果运行的是B固件,则需要选择A固件进行更新),我们选择Shared File。

    5. 固件选择

        选择B固件,进行升级。

    6. 正在升级

    7.  升级完成






  • 你好,我想问下,BLE协议栈,开启低功耗的话,除了要在IAR下将POWER_SAVING设置TRUE,还需要作其它设置吗?

  • 有一个关于SBL的问题请教

    发现每次使用 SerialBootTool.exe 下载 BIn 时,都需要先使用 Flash Programmer 下载SBL_CC254xF256_UART0_ALT1.hex ,如果这样,还不如直接使用 Flash Programmer 下载整个程序。

  • Dear zhaoqi zhu 你好,請問一下我是andriod 手機,文章中提到的那個truthblue 是 app ? 在Google Play 找的到嗎? 小弟是使用 TI 自己開發的 for andriod app SensorTag 與 CC2540 做配對,可一直Scan 不到 CC2540,我同事iPhone 安裝TI Sensor Tag 就Scan的到 CC2540,看到此文章提到 for andriod truthblue 可以Scan 的到 CC2540,想用在我手機上試看看,可以告訴我要去那下載安裝嗎? 謝謝。  

  • 您好,我们安卓端所有APP源码都是公开,可以在:

    http://pan.baidu.com/share/link?shareid=3562495290&uk=3996269986#dir/path=%252F%25E6%25B7%25B1%25E5%259C%25B3%25E5%25B8%2582%25E9%25A6%2592%25E5%25A4%25B4%25E7%25A7%2591%25E6%258A%2580%25E6%259C%2589%25E9%2599%2590%25E5%2585%25AC%25E5%258F%25B8%252F%25E9%25A6%2592%25E5%25A4%25B4%25E7%25A7%2591%25E6%258A%2580%25E8%2593%259D%25E7%2589%25994.0%25E7%25B3%25BB%25E5%2588%2597

    下载

  •  您好,谢谢大大分享。网盘已失效,能再更新个地址否?
    OR,zfeng571@163.com邮件我,不胜感激 

  • 楼主你好,网盘的资料不存在了,请问还有别的地方可以下载吗?

    或者帮忙发我一份可以吗?我的邮箱是:mcu_k001@163.com

  • 你好!由于链接失效了,可以发我邮箱一份么?不胜感激!1093701248@qq.com

  • 您好!能不能再发一下学习教程呢???也可以发邮箱1578763438@qq.com

  • l楼主 网盘里面文件失效了 能发一份到我的邮箱吗 谢谢  lxdfrank@163.com

  • 有没有usb控制芯片相关的,比如:一个linux主机通过usb获取当前usb dongle中相关信息并且可以命令控制usb dongle.

  • 请问 从机固件路径在哪里能找到呢?谢谢

  • 网盘的资料能否发一份啊,在学习

  • 您好,请问您之前下载过资料吗?能否给一份啊

  • 您好这份资料您下载了吗?能不能分享一下啊?邮箱是7292628926@qq.com

  • 资料的链接失效了,能不能再发一次新的