4.1 HAL的文件结构和工程结构
硬件适配层(Hardware Adaptation Layer,HAL),用于适配各种各样的硬件,为上层提供统一的接口,从而方便开发者使用各种各样的硬件。本节课从文件结构和工程结构这两方面简单地讲解HAL的结构。
HAL的文件结构
HAL的文件结构较为复杂,读者只需要跟笔者的讲解思路简单地了解一下就可以了。
在安装好Z-Stack 3.0 后,HAL相关的源代码可以在Z-Stack 3.0的Components文件夹中找到,如图所示。
进入hal目录,如图所示。
其中的文件夹说明如下:
(1)common:存放公共文件。
(2)include:存放各种驱动程序接口文件。
(3)target:存放各种类型主板的驱动程序源文件。
进入common文件夹,如图所示。
common目录中的文件说明如下:
(1)hal_assert.c:实现条件合法性判断功能的源代码文件。
(2)hal_drivers.c:硬件抽象层HAL任务初始化及事件处理函数所在文件,是HAL的入口源文件。
返回上一级,并进入include目录,如图所示。
include目录中的文件描述如下:
(1)hal_adc.h:ADC(模拟数字转换)驱动程序头文件。
(2)hal_assert.h:条件合法性判断功能的头文件。
(3)hal_board.h:各种类型主板的硬件资源配置头文件。
(4)hal_defs.h:通用定义头文件。
(5)hal_drivers.h:HAL任务初始化及事件处理函数的头文件。
(6)hal_flash.h:FLASH(存储器)驱动程序头文件。
(7)hal_key.h:按键驱动程序头文件。
(8)hal_lcd.h:显示屏驱动程序头文件。
(9)hal_led.h:LED驱动程序头文件。
(10)hal_rpc.h:RPC(Remote Procedure Call,远程过程调用)驱动程序头文件。
(11)hal_sleep.h:休眠功能驱动程序头文件。
(12)hal_timer.h:定时器驱动程序头文件。
(13)hal_uart.h:串口驱动程序头文件。
返回上一级,然后进入target文件夹,如图所示。
target目录中的文件夹描述如下:
(1)CC2530EB:针对芯片主控为CC2530的评估板相关的驱动程序。
(2)CC2530USB:针对芯片主控为CC2530的带USB转串口评估板的驱动程序。
(3)CC2530ZNP:针对芯片主控为CC2530的ZNP(ZigBee And Processor)评估板的驱动程序。
(4)CC2538:针对芯片主控为CC2538的评估板的驱动程序。
(5)CC2538ZNP:针对芯片主控为CC2538的ZNP(ZigBee And Processor)评估板的驱动程序。
以CC2530EB为例,进入到这个目录,如图所示。
这个目录下中存放两种类型的硬件驱动程序源文件:
- 一种是实现了刚才介绍过的include目录中的接口的源代码文件,例如hal_led、hal_lcd.c和hal_adc.c等等。
- 另一种是与当前的主板类型相关的驱动程序源文件,比如DMA(Direct Memory Access,直接存储器访问)、OTA(Over The Air,空中升级)等。
简单地讲解一下这个目录中的各个文件:
(1)_hal_uart_dma.c:串口DMA(Direct Memory Access,直接存储器访问)驱动程序源文件。
(2)_hal_uart_isr.c:串口ISR(Interrupt Service Routines,中断服务程序)驱动程序源文件。
(3)hal_adc.c:ADC(模拟数字转换)驱动程序源文件
(4)hal_aes.h:AES(Advanced Encryption Standard,高级加密标准)驱动程序头文件。
(5)hal_board_cfg.h:当前主板的硬件资源配置头文件。
(6)hal_ccm.h:CCM(Counter with CBC-MAC,分组密码链接-消息认证码)驱动程序头文件。
(7)hal_dma.c:DMA(Direct Memory Access,直接存储器访问)驱动程序源文件。
(8)hal_dma.h:DMA(Direct Memory Access,直接存储器访问)驱动程序头文件。
(9)hal_flash.c:FLASH(存储器)驱动程序源文件。
(10)hal_key.c:按键驱动程序源文件。
(11)hal_lcd.c:显示屏驱动程序接口文件。
(12)hal_led.h:LED(发光二极管)驱动程序头文件。
(13)hal_mac_cfg.h:MAC(Medium Access Control,媒体介质访问控制)相关配置头文件。
(14)hal_mcu.h:与主控芯片相关的驱动程序头文件。
(15)hal_oad.c:OAD(On Air Download,空中下载)驱动程序源文件。
(16)hal_oad.h:OAD(On Air Download,空中下载)驱动程序头文件。
(17)hal_ota.c:OTA(Over The Air,空中升级)驱动程序源文件。
(18)hal_ota.h:OTA(Over The Air,空中升级)驱动程序头文件。
(19)hal_sleep.c:休眠功能驱动程序源文件。
(20)hal_startup.c:系统启动相关的驱动程序源文件。
(21)hal_timer.c:定时器驱动程序源文件。
(22)hal_types.h:类型定义头文件。
(23)hal_uart.c:串口驱动程序源文件。
HAL工程结构
本节内容将以SampleSwitch例程为例来讲解其中的HAL的工程文件结构。
工程组织结构
打开SampleSwitch工程,可以找到HAL所在目录,如图所示。
分别展开Common、Include和Target目录,如图所示。
Target目录中有一个CC2530EB目录,表示这个工程采用的是CC2530EB主板相关的驱动程序文件。
在CC2530EB目录中包含Config、Drivers和Includes这3个目录。Config目录存放了该主板的硬件资源配置头文件,Drivers目录存放了各种驱动程序源文件,Includes目录存放了各种驱动程序头文件。
4.2 HAL的架构简介
本节课继续以SampleSwitch例程为例来讲解HAL的架构。类似地,读者只需要跟随笔者的讲解思路大致了解一下本节课的内容就可以了。
HAL的架构简介
初始化
HAL的初始化函数Hal_Init()在hal_drivers.c文件中,代码如下:
/**************************************************************************************************
* @fn Hal_Init
*
* @brief Hal Initialization function.
*
* @param task_id - Hal TaskId
*
* @return None
**************************************************************************************************/
void Hal_Init( uint8 task_id )
{
/* Register task ID */
Hal_TaskID = task_id;
#ifdef CC2591_COMPRESSION_WORKAROUND
osal_start_reload_timer( Hal_TaskID, PERIOD_RSSI_RESET_EVT, PERIOD_RSSI_RESET_TIMEOUT );
#endif
}
驱动程序初始化
在hal_drivers.c文件中,可以找到驱动初始化函数HalDriverInit(),这个函数的主要作用是针对各种硬件外设进行初始化,代码如下:
/**************************************************************************************************
* @fn Hal_DriverInit
*
* @brief Initialize HW - These need to be initialized before anyone.
*
* @return None
**************************************************************************************************/
void HalDriverInit (void)
{
//定时器,通过设置宏定义HAL_TIMER为TRUE来使能该功能
#if (defined HAL_TIMER) && (HAL_TIMER == TRUE)
#endif
//模数转换功能,通过设置宏定义HAL_ADC为TRUE来使能该功能
#if (defined HAL_ADC) && (HAL_ADC == TRUE)
HalAdcInit();
#endif
//DMA(直接存储器访问),通过设置宏定义HAL_DMA为TRUE来使能该功能
#if (defined HAL_DMA) && (HAL_DMA == TRUE)
HalDmaInit();
#endif
//AES(高级加密标准),通过设置宏定义HAL_AES为TRUE来使能该功能
#if (defined HAL_AES) && (HAL_AES == TRUE)
HalAesInit();
#endif
//显示器,通过设置宏定义HAL_LCD为TRUE来使能该功能
#if (defined HAL_LCD) && (HAL_LCD == TRUE)
HalLcdInit();
#endif
//LED,通过设置宏定义HAL_LED为TRUE来使能该功能
#if (defined HAL_LED) && (HAL_LED == TRUE)
HalLedInit();
#endif
//串口,通过设置宏定义HAL_UART为TRUE来使能该功能
#if (defined HAL_UART) && (HAL_UART == TRUE)
HalUARTInit();
#endif
//按键,通过设置宏定义HAL_KEY为TRUE来使能该功能
#if (defined HAL_KEY) && (HAL_KEY == TRUE)
HalKeyInit();
#endif
//SPI,通过设置宏定义HAL_SPI为TRUE来使能该功能
#if (defined HAL_SPI) && (HAL_SPI == TRUE)
HalSpiInit();
#endif
//HID(Human Interface Device),通过设置宏定义HAL_HID为TRUE来使能该功能
#if (defined HAL_HID) && (HAL_HID == TRUE)
usbHidInit();
#endif
}
在实际的开发过程中,开发者可以根据需求来使用指定的外设。在这段代码中可以看出,需要用指定的外设就定义对应宏就可以了,例如如果要用LED,那么就去定义HAL_LED这个宏为TRUE,如果不用用到,那么就不用定义。在定义了对应的宏后就会执行对应的初始化,例如这个LED初始化函数HalLedInit()。后续的章节将会讲述具体的宏定义方法。
开发者如果需要新增其他的外设,那么可以按照协议栈的这个架构新增外设
事件处理
HAL的事件处理函数Hal_ProcessEvent()在hal_drivers.c文件中,它的主要作用是处理HAL层的事件,代码如下:
/**************************************************************************************************
* @fn Hal_ProcessEvent
*
* @brief Hal Process Event
*
* @param task_id - Hal TaskId
* events - events
*
* @return None
**************************************************************************************************/
uint16 Hal_ProcessEvent( uint8 task_id, uint16 events )
{
uint8 *msgPtr;
(void)task_id; // Intentionally unreferenced parameter
if ( events & SYS_EVENT_MSG )
{
//省略系统事件的处理代码
.........
return events ^ SYS_EVENT_MSG;
}
//蜂鸣器事件,需要宏定义HAL_BUZZER为TRUE才使能该事件
#if (defined HAL_BUZZER) && (HAL_BUZZER == TRUE)
if (events & HAL_BUZZER_EVENT)
{
HalBuzzerStop();
return events ^ HAL_BUZZER_EVENT;
}
#endif
//RSSI重置事件,需要宏定义PERIOD_RSSI_RESET_EVT为TRUE才使能该事件
#ifdef CC2591_COMPRESSION_WORKAROUND
if ( events & PERIOD_RSSI_RESET_EVT )
{
macRxResetRssi();
return (events ^ PERIOD_RSSI_RESET_EVT);
}
#endif
//LED闪烁事件
if ( events & HAL_LED_BLINK_EVENT )
{
#if (defined (BLINK_LEDS)) && (HAL_LED == TRUE)
HalLedUpdate();
#endif /* BLINK_LEDS && HAL_LED */
return events ^ HAL_LED_BLINK_EVENT;
}
//按键事件
if (events & HAL_KEY_EVENT)
{
#if (defined HAL_KEY) && (HAL_KEY == TRUE)
/* Check for keys */
HalKeyPoll();
/* if interrupt disabled, do next polling */
if (!Hal_KeyIntEnable)
{
osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);
}
#endif
return events ^ HAL_KEY_EVENT;
}
//低功耗事件,需要宏定义HAL_SLEEP_TIMER_EVENT为TRUE才使能该事件
#if defined POWER_SAVING
if ( events & HAL_SLEEP_TIMER_EVENT )
{
halRestoreSleepLevel();
return events ^ HAL_SLEEP_TIMER_EVENT;
}
if ( events & HAL_PWRMGR_HOLD_EVENT )
{
(void)osal_pwrmgr_task_state(Hal_TaskID, PWRMGR_HOLD);
(void)osal_stop_timerEx(Hal_TaskID, HAL_PWRMGR_CONSERVE_EVENT);
(void)osal_clear_event(Hal_TaskID, HAL_PWRMGR_CONSERVE_EVENT);
return (events & ~(HAL_PWRMGR_HOLD_EVENT | HAL_PWRMGR_CONSERVE_EVENT));
}
if ( events & HAL_PWRMGR_CONSERVE_EVENT )
{
(void)osal_pwrmgr_task_state(Hal_TaskID, PWRMGR_CONSERVE);
return events ^ HAL_PWRMGR_CONSERVE_EVENT;
}
#endif
return 0;
}
HAL轮询
在hal_drivers.c文件中,可以找到Hal_ProcessPoll()函数,这个函数的主要作用是轮询那些需要快速处理的功能模块,代码如下:
/**************************************************************************************************
* @fn Hal_ProcessPoll
*
* @brief This routine will be called by OSAL to poll UART, TIMER...
*
* @return None
**************************************************************************************************/
void Hal_ProcessPoll ()
{
//是否进入低功耗模式
#if defined( POWER_SAVING )
ALLOW_SLEEP_MODE();
#endif
//串口
#if (defined HAL_UART) && (HAL_UART == TRUE)
HalUARTPoll();
#endif
//SPI
#if (defined HAL_SPI) && (HAL_SPI == TRUE)
HalSpiPoll();
#endif
//HID
#if (defined HAL_HID) && (HAL_HID == TRUE)
usbHidProcessEvents();
#endif
}
到这里,已经为读者简单地介绍了HAL的整体结构,接下来将会讲解如何利用HAL来使用LED、按键、串口、显示器和ADC等基本硬件资源。
4.2 LED API简介
相关 API 说明
打开SampleSwitch这个工程,在hal_led.h文件中可以找到操作LED的API,文件所在位置如图所示。
在这个头文件中,有3个常用的API,代码如下:
/*
* 初始化LED
*/
extern void HalLedInit( void );
/*
* 开关LED
*/
extern uint8 HalLedSet( uint8 led, uint8 mode );
/*
* 闪烁LED
*/
extern void HalLedBlink( uint8 leds, uint8 cnt, uint8 duty, uint16 time );
HalLedInit()的作用是初始化LED。在上一章节中,我们讲解过在hal_drivers.c文件中的驱动程序初始化函数HalDriverInit()里面调用了这个函数。
在Z-Stack 3.0中,HAL默认支持了4个LED,LED的逻辑定义如下:
/* LEDS - The LED number is the same as the bit position */
#define HAL_LED_1 0x01
#define HAL_LED_2 0x02
#define HAL_LED_3 0x04
#define HAL_LED_4 0x08
#define HAL_LED_ALL (HAL_LED_1 | HAL_LED_2 | HAL_LED_3 | HAL_LED_4)
HalLedSet( uint8 led, uint8 mode )用来设定LED的工作模式。其中的第1个参数led用于指定待设定的LED,第2个参数mode用于指定工作模式,这个工作模式有3种:
(1)开启,对应的宏定义是HAL_LED_MODE_ON。
(2)关闭,对应的宏定义是HAL_LED_MODE_OFF。
(3)反转,对应的宏定义是HAL_LED_MODE_TOGGLE。
例如如果要开启第1盏LED,可以按这个方式调用:
HalLedSet(
HAL_LED_1,//指定第1盏LED
HAL_LED_MODE_ON);//开启
HalLedBlink( uint8 leds, uint8 cnt, uint8 duty, uint16 time )的作用是让LED闪烁。其中的第1个参数leds用于指定LED,第2个参数cnt用于指定闪烁的次数,第3个参数duty用于指定LED是开启状态时的占空比,第4个参数time用于指定每次闪烁的时间周期(单位:ms)。
举个例子说明一下使用方法,例如如果需要让第1盏LED闪烁10次,每次闪烁的时间周期是2000ms,而且在这2000ms中有60%的时间LED要处于开启状态,那么可以按照以下方式调用:
HalLedBlink(
HAL_LED_1,//指定第1盏LED
10,//指定闪烁次数是10次
60,//指定60%的时间LED是处于开启状态
2000);//指定1次闪烁的时间周期是2000ms,也就是在1次闪烁周期中,LED的开启时长是1200ms,关闭时长是800ms
LED的物理映射
在调用LED API之前,需要配置好LED的物理映射定义。LED的物理映射定义在hal_board_cfg.h文件中,文件所在位置如图所示。
默认的LED的物理映射定义如下:
//LED1
#define LED1_BV BV(0)
#define LED1_SBIT P1_0
#define LED1_DDR P1DIR
#define LED1_POLARITY ACTIVE_HIGH
#if defined (HAL_BOARD_CC2530EB_REV17)
//LED2
#define LED2_BV BV(1)
#define LED2_SBIT P1_1
#define LED2_DDR P1DIR
#define LED2_POLARITY ACTIVE_HIGH
//LED3
#define LED3_BV BV(4)
#define LED3_SBIT P1_4
#define LED3_DDR P1DIR
#define LED3_POLARITY ACTIVE_HIGH
#ifdef ENABLE_LED4_DISABLE_S1
//LED4
#define LED4_BV BV(1)
#define LED4_SBIT P0_1
#define LED4_DDR P0DIR
#define LED4_POLARITY ACTIVE_HIGH
#define LED4_SET_DIR() do {LED4_DDR |= LED4_BV;} while (0)
#else
#define LED4_SET_DIR()
#endif
#endif
这里以LED1为例讲解一下物理映射定义的含义。
(1)其中的BV(N)表示数值1向左移动N位,等价于(1 << N),因此BV(0)的值为1<<0,也就是等于1。
<<是向左移位运算符,1<<0表示数值1向左移动0个位,移位后的值不变,仍等于1。举多一个例子,例如数值1的二进制数是0000 0001,1<<2表示把0000 0001中的所有位向左移动两位,并且在低位补0、把移出的高位丢弃,所以计算结果也就是0000 0100,对应的十进制数是3。如果读者不了解这个运算符,需要补充一下相关的知识。
(2)#define LED1_SBIT P1_0表示LED1与CC2530的P1_0连接。
(3)#define LED1_DDR P1DIR表示LED1对应的方向寄存器为P1DIR。P1DIR在前面的章节已经讲解过,这里不再讲解了。
(4)#define LED1_POLARITY ACTIVE_HIGH表示LED1是高电平驱动的,也就是给P1_0输入高电平,LED1就会被点亮。
这些是协议栈的默认配置,开发者需要根据实际的硬件连接情况进行修改。例如,配套的ZigBee开发板是把LED连接到P0_4的,因此可以把LED1的引脚修改为P0_4,修改后代码如下:
//LED1
#define LED1_BV BV(4)
#define LED1_SBIT P0_4
#define LED1_DDR P0DIR
#define LED1_POLARITY ACTIVE_HIGH
剩下的LED2、LED3和LED4由于没有用到,因此可以暂时忽略。
4.3 LED 实验
前面的章节讲解了事件的应用,那么本节课利用事件机制来实现闪烁LED的效果。
重定义LED的物理映射
在hal_led.h文件中,重定义LED1的物理映射定义,代码如下:
//LED1
#define LED1_BV BV(4)
#define LED1_SBIT P0_4
#define LED1_DDR P0DIR
#define LED1_POLARITY ACTIVE_HIGH
因为配套的ZigBee开发板是把LED连接到P0_4的,所以这里把LED1的引脚修改为P0_4。
LED API的应用
在讲解OSAL的章节中,自定义了一个SAMPLEAPP_TEST_EVT事件,并且编写了对应的事件处理代码,本节课把事件处理代码修改为闪烁LED。
回到应用层,在zcl_samplesw.c文件的事件处理函数zclSampleSw_event_loop()中修改自定义事件SAMPLEAPP_TEST_EVT的处理逻辑,代码如下:
// 处理自定义的用户事件:SAMPLEAPP_TEST_EVT
if ( events & SAMPLEAPP_TEST_EVT )
{
printf("Blink LED!\r\n");
HalLedBlink(
HAL_LED_1,//指定第1盏LED
10,//指定闪烁次数是10次
50,//指定50%的时间LED是处于开启状态
1000);//指定1次闪烁的时间周期是1000ms
//消除已经处理的事件,然后返回未处理的事件
return ( events ^ SAMPLEAPP_TEST_EVT );
}
启用LED宏定义
前面章节曾经讲到,需要用到哪些外设功能就去定义对应的宏,那么现在需要用到LED功能,也就是需要去定义LED功能对应的宏就可以了。右击工程名字,然后选择Options,打开工程配置,如图所示。
在工程设置中选择C/C++ Compiler→Preprocessor,在Defined symbols中添加HAL_LED=TRUE,然后点击OK,如图所示。
代码测试
(1)使用配套的仿真器把ZigBee开发板连接到电脑上。
(2)点击Download and Debug按钮进行程序的编译、链接和下载并进入仿真模式,如图所示。
(3)进入仿真模式之后,点击Go按钮运行程序,如图所示。
(4)程序运行后会在Terminal I/O窗口中输出“Blink LED!”,如图所示。
(5)与此同时,可以在ZigBee开发板中观察到与P0_4连接的LED闪烁了10次。