jijuxie321 发表于 2009-7-25 22:07:00

如何在MCU中实现一个基本的HAL?

昨天写了这个:UC/OS-II,多任务喂狗实现,今天紧接着写HAL,不过比较懒只写了一半,明天再继续。
http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn=3477082&bbs_page_no=3&bbs_id=9999

如何在MCU中实现一个基本的HAL??
                                                        墨鱼 QQ312008063

MCU资源紧张,为什么我还要费那么大劲把软件搞复杂呢?
也许你会问单片机这东西就那么点资源,运算速度又不够快。那么在这上面多了一层抽象自然就会降低效率,为什么还要这样做呢。
我这个人有时候兴趣比工作重要,所以去年各行各业大裁员的时候我也很幸运的被照顾到了。这不能怪谁,只能怪我自己。不过其实也不错,我收拾完心情重新找工作。问题是这回我还是不能如愿。我一直都想找份从事ARM9,LINUX或者WINCE类的工作。自学不是不可能,只是那样子花的时间多,效率也低。你问我为什么想学这些,其实不为别的,单纯为了兴趣。说实话本人基础很是一般。学历也不高,才大专。所以工作一直都不是很理想。当然现在比以前好了。至少每个月省一点花还能存点钱。将来回老家开家店之类的,这些都是废话,有点跑题了。所以我并不想在研发这一行长期待下去,趁年轻多学点东西,将来我就可以把这个当成单纯的兴趣爱好。就像刚毕业那会。
所以通过实现HAL,帮助我理解操作系统如何实现外设的驱动,当然LINUX肯定是更复杂。但通过这个小东西再去接触LINUX驱动就会简单明了许多。况且现在32位MCU越来越普及,从个人爱好的角度出发,资源不足我可以换个FLASH空间更大内存更多的MCU。所以我当初选了LPC2148。以后可能会换STM32。为了保证我将来所集成的上层软件模块(例如LWIP,FAT文件系统,GUI)能很轻易的在各种单片机上运行,所以也需要这么一个东西。

那么废话讲完了,现在进入正题,首先是一般来说外设的共性是什么:
        外设在软件工程师眼里,无非就是一堆的寄存器。有些用来控制外设的行为,有些用来获取外设的运行状态。有些是数据的通路。所以设备的驱动程序其实也没什么复杂的。相对一些需要用数学来实现的算法是相当简单的了,都是逻辑而以。
        既然外设如此简单,而且又知道了它们的共性。那么怎么实现我们的硬件抽象层呢?
首先来看这样一个结构体:
typedef struct device_function{
uint8 (*DevPutch)(uint8 *str, uint16 legth);          //设备字节写
uint8 (*DevGetch)(uint8 *str, uint16 legth);          //设备字节读
uint8 (*DevControl)(void* param);                                        //设备控制函数       
}DEV_FUN;
刚才我们说了,设备的共性就是有些寄存器是用来控制外设的行为的,所以注意第三个结构体成员uint8 (*DevControl)(void* param);这是一个指向设备控制函数的指针,它接收一个void*类型的指针。我们知道void*类型的指针是一个通用指针,它不对具体类型进行检查。你可以传递任何东西给它。这一点很是关键,因为设备千差万别,每一种设备的控制方法都不太一样。通过该指针,我们将向DevControl所指向的具体外设控制函数传递一系列控制参数,然后底层设备控制函数在接收到控制参数再利用强制转换的方式获得实际参数类型。


以UART为例:
/************************************************************************
函数名称: u_UART0Control
函数功能:串口0控制函数
输    入:void* param
输    出:
作    者:陈国忠
*************************************************************************/
uint8 UART0_Control(void* param){

        UART_CONTROL_PARAM* uartParam = (UART_CONTROL_PARAM*)param;
        ………………..//以下省略,后面会针对每个外设如何编写驱动时再详细说明。
}
如上,通过强制转换我们获得了UART_CONTROL_PARAM指针类型,然后就知道该类型内各成员变量的偏移量。就可以从这些偏移量中取得各个参数。
UART_CONTROL_PARAM 类型定义如下:

//定义了一个串口模式设定结构体.
typedef struct UartMode{
        BYTE_LEN                size;//串口通信字长度.
        STOP_BIT_LEN        stopb;//停止位数
        CHECK_BIT_MODE        parity;//奇偶较验类型.
        PARITY_CHECK_BIT_EN         enparity;//使能或禁止奇偶较验.
}UARTMODE;

//UART控制参数
typedef struct uart_control_param        UART_CONTROL_PARAM;
struct uart_control_param{
        uint32        BaudRate; //串口波特率
        UARTMODE Set;          //串口模式设置
        uint8        wait_out_time; //等待超时时间
};

当然以后还会有IIC,SPI等等各种外设的控制参数被定义出来。

紧接着我们来看第一,第二个成员变量:
uint8 (*DevPutch)(uint8 *str, uint16 legth);          //设备字节写
uint8 (*DevGetch)(uint8 *str, uint16 legth);          //设备字节读
这两个就是我们的数据通道了,一个实现指定字节的写,一个实现指定字节的读。

那么在多任务下实现设备抽象以外我们还要做什么呢?
在多任务环境下,可能会有多个任务共同操作一个设备,所以需要对设备的使用权进行管理,也就是设备互斥。所以我给这个结构再添加一个成员,变成这样子:
struct device_operations_list{       //设备控制函数列表
uint8 (*DevPutch)(uint8 *str, uint16 legth);          //设备字节写
uint8 (*DevGetch)(uint8 *str, uint16 legth);          //设备字节读
uint8 (*DevControl)(void* param);                                        //设备控制函数
OS_EVENT** pDevFile;        //指向设备互斥管理文件指针                                       
};
不好意思,第四个成员变量,命名不太贴切,不过我也不想改过来了。它是一个指针的指针,为什么用指针的指针????这个后面再介绍。它是一个指向OS_EVENT*指针,的指针。在获取一个设备控制权的时候将通过它找到用于管理该设备的互斥信号量(或初值为1的计数信号量)。
审请设备使用权的时候将对计数信号量的值减一,这样其它设备再次审请时就只能等待。

接下来我们需要开始管理不同的设备,那么就需要识别不同的设备,所以为每个设备分配一个ID是必需的。所以我又增加了两个成员:

struct device_control_block{   
DEV_ID        deviceId;        //设备ID
uint8        openDevOutTime; //打开设备超时时间
uint16        NOUSE;       
uint8 (*DevPutch)(uint8 *str, uint16 legth);          //设备字节写
uint8 (*DevGetch)(uint8 *str, uint16 legth);          //设备字节读
uint8 (*DevControl)(void* param);                                        //设备控制函数
OS_EVENT** pDevFile;        //指向设备互斥管理文件指针                                       
};

好了,往这里添加不同的设备ID吧。
typedef enum{
        EMPTY_ID,        //无效的设备ID
        UART0_ID,       
        UART1_ID,
        I2C0_ID,
        SPI0_ID,
        SPI1_ID,
        MAX_DEV_MUN        //
}DEV_ID;


然后声明,定义外设备管理所使用的设备控制块列表:
考虑到单片机RAM资源是相当宝贵,所以设备的控制块采取静态分配形式,也就是全部放在FLASH里面。这就是为什么OS_EVENT** pDevFile 是一个指针的指针原因了,因为如果它是一个静态的指针,那它就不能被改变,而我们需要调用创建信号量来获得一个计数信号量。所以利用一个不可改变的静态指针(OS_EVENT** pDevFile)去指向另一个可以被改变的指针变量。

//静态分配设备控制块,节省内存使用量
const DEV_CONTROL_BLOCK DevControlBlock = {
       
        #ifdef EN_UART0_DEVICE
        {
               UART0_ID,
                0x00,                //0x00表示无限时等待一个设备打开
                0x0000,                //未使用保留,地址对齐
          &UART0_SendNByte,
                &UART0_GetNByte,
                &UART0_Control,
                &UART0_FILE,
        },
        #endif
        ………………………………..//中间省略一些具体外设设备控制块内容。
        #ifdef EN_SPI1_DEVICE
        {       
                SPI1_ID,
                0x00,
                0x0000,
                &SPI1_SendNByte,
                &SPI1_GetNByte,
                &SPI1_Control,
                &SPI1_FILE,
        },
        #endif
        {
          MAX_DEV_MUN,
                0x00,
                0x00,
                NULL,
                NULL,
                NULL,
                NULL,
        }
};

该如何获得设备使用权:
所谓获得设备使用权就是要获取指向底层设备驱动的三个函数指针,DevPutch、DevGetch、DevControl。但必需尊寻设备互斥原则。
所以HAL提供了两个接口,分别用于打开设备和关闭设备:
/************************************************************************
函数名称:DeviceOpen
函数功能:以任务独占方式打开一个设备文件,当一个设备被占用时其它任务只有等
               待,直到占有设备的任务调用关闭设备文件函数释放该设备.
输    入:
输    出:返回设备操作句柄
作    者:
*************************************************************************/

const DEV_FUN* DeviceOpen(DEV_ID devId, HAL_ERR_CODE* err){
        const DEV_CONTROL_BLOCK * GetHCB = DevControlBlock;

        uint8 OSerr;
        OS_EVENT *devFile;

        while(GetHCB < &DevControlBlock){

                if(GetHCB->deviceId == devId){
                       
                        devFile = *(GetHCB->pDevFile);
                       
                        OS_ENTER_CRITICAL();
                        if(devFile->OSEventType == MUTEX_SEM){
                                OSMutexPend(devFile, GetHCB->openDevOutTime, &OSerr);
                        }else if(devFile->OSEventType == BASIC_SEM){
                                OSSemPend(devFile, GetHCB->openDevOutTime, &OSerr);
                        }else{
                                OS_EXIT_CRITICAL();
                                *err = HAL_NO_FIND_DEV;
                                return NULL;
                        }
                        OS_EXIT_CRITICAL();

                        if(OSerr != OS_NO_ERR){
                                *err = HAL_WAIT_DEV_TIME_OUT;
                                return NULL;
                        }
                        *err = HAL_NO_ERR;
                        return (const DEV_FUN*)(&(GetHCB->DevPutch));
                }
                GetHCB++;       
        }

        *err = HAL_NO_FIND_DEV;
        return NULL;
}
如上所示,根据设备ID搜索设备控制块列表,找到相关设备控制块,然后审请信号量获得使用权限后返回设备操作的三个函数指针首地址。

该是归还设备使用权的时候又怎么做呢。
/************************************************************************
函数名称:DeviceClose
函数功能:关闭一个设备文件
输    入:const DEV_FUN** dev
输    出:
作    者:
*************************************************************************/
uint8 DeviceClose(const DEV_FUN** dev){
        const DEV_OPERATIONS* GetDev = NULL;

        OS_EVENT *devFile;

        uint8 OSerr = 1;
       
        GetDev = (DEV_OPERATIONS *)(*dev);//类型强制转换,获得设备信号量偏移。
        if(GetDev == NULL){
                return HAL_CLOSE_DEV_FAIL;
        }

        devFile = *(GetDev->pDevFile);
        if(devFile == NULL){
                return HAL_CLOSE_DEV_FAIL;
        }

        OS_ENTER_CRITICAL();
        if(devFile->OSEventType == MUTEX_SEM){//根据信号量类型,释放信号量。
                OSerr = OSMutexPost(devFile);
        }else if(devFile->OSEventType == BASIC_SEM){
                OSerr = OSSemPost(devFile);
        }else{
                OS_EXIT_CRITICAL();
                return HAL_NO_FIND_DEV;
        }
        OS_EXIT_CRITICAL();

        if(OSerr == OS_NO_ERR){
                *dev = NULL;
                return HAL_NO_ERR;
        }
        else
        {
                return HAL_CLOSE_DEV_FAIL;
               
        }

}
归还设备使用权限的时候也很简单,这里不需要根据ID搜索设备控制块,因为直接通过设备函数首地址“const DEV_FUN*”指针强制转换成“const DEV_OPERATIONS*”指针就能释放信号量。

提供更友好的上层接口函数:
调用设备打开,获得指向底层驱动的三个函数指针,已经可以对底层进行最基本的操作了。但这样的接口形式显然很不友好。所以在这个基础上我们需要为上层应用程序提供固定,统一人接口函数。
例如串口的格式化输出:
void v_MiniPrintf(const DEV_FUN *Dev,char *fmt,...);

I2C的主,从模式读写:
I2CERRCODE I2CMasterSend(const DEV_FUN* Dev,uint8 DeviceAddr,uint16 Suba,uint8 *str,uint16 length);
I2CERRCODE I2CMasterReceive(const DEV_FUN* Dev,uint8 DeviceAddr,uint16 Suba,uint8 *str,uint16 length);
I2CERRCODE I2CSlaverReceive(const DEV_FUN* Dev,uint8 *DataAddr,uint8 *str,uint16 length);

还有SPI的主从模式读写等等
SPIERRCODE SPIClkSet(const DEV_FUN* Dev,uint32 FSPI);
SPIERRCODE SPIMasterSend(const DEV_FUN* Dev,SCHEDULING spi_data_format, SPI_SEND_BIT_LEN bit_len,uint8 *str,uint16 length);

……………………………………….


接下来提供一个例子,说明上层抽象接口是如何利用”const DEV_FUN *Dev”
void v_MiniPrintf(const DEV_FUN *Dev,char *fmt,...){
        va_list ap;
        int8 strval;
        int8 nval;
        char *p;
        char *PointChar;
        static int8 i;
       
        if(Dev == NULL){
                return ;
        }
        va_start(ap,fmt);
        for(p = fmt; *p; p++){
                if(*p != '%'){
                        Dev->DevPutch((uint8*)p,1);
                        continue;
                }       
                p++;
                switch(*p){
                                        case 'd': nval = va_arg(ap,int);
                                                          i=0;
                                                          do{
                                                                          strval = nval % 10;
                                                                          nval /= 10;
                                                                          i++;
                                                          }while(nval>0);
                                                          i--;
                                                          break;
                                        case 'x': nval = va_arg(ap,int);
                                                          i = 0;
                                                          do{
                                                                          strval = nval % 16;
                                                                          nval /= 16;
                                                                          i++;
                                                          }while(nval > 0);
                                                          i--;       
                                                          break;
                                        case 'c': i = -1;
                                                          nval = va_arg(ap,int);
                                                          Dev->DevPutch((uint8*)&nval,1);
                                                          break;       
                                        case 's':i = -1;
                                                       PointChar = (char *)va_arg(ap,int);
                                                       nval = *PointChar;
                                                       while(nval){
                                                               nval = *PointChar;
                                                               if(nval!=0){
                                                                       Dev->DevPutch((uint8*)PointChar,1);
                                                               }
                                                                PointChar++;
                                                       }
                                                       break;
                                        default : break;                                                                 
                }               
                for( ;i >= 0;i--){
                        nval = strval+48;
                        Dev->DevPutch((uint8*)&nval,1);                //调用底层字符串输出
                }
        }
        va_end(ap);       

}

占位:底层驱动要完成的工作。

pigathfut 发表于 2009-7-25 22:10:33

mark

jswk 发表于 2009-7-25 22:37:00

标记~干得不错!

fugeone 发表于 2009-7-26 07:24:53

还真是复杂啊

csclz 发表于 2009-7-26 07:29:44

真是复杂

LiAsO 发表于 2009-7-26 07:59:59

~~~~~~~~~~~楼主这段话我喜欢,直中目标

外设在软件工程师眼里,无非就是一堆的寄存器。
有些用来控制外设的行为,有些用来获取外设的运行状态。
有些是数据的通路。所以设备的驱动程序其实也没什么复杂的
~~~~~~~~~~~
在一本关于arm和设备驱动的书里看到的程序,基本就是楼主描述的结构体形式,
我一直在想,用arm编程思想(arm9以上)来指导8位单片机编程,是不是有种游刃有余的错觉?(或是别的?)
ps我纸上谈兵了,勿怪。

newbier 发表于 2009-7-26 08:30:46

HAL的好处是使用起来方便,因为与底层硬件完全隔离开了,但如果出现问题调试起来就比较麻烦了。

jijuxie321 发表于 2009-7-26 15:49:15

只是一部分,后面还有GPIO的抽像,外部中断管理(实现两层逻辑中断管理)

oldtom 发表于 2009-7-26 20:24:41

建议楼主用LPC2478 跑ucos+lwip

jijuxie321 发表于 2009-7-26 23:03:02

呵呵。。不了。以前我有一块LPC2468(忘记了。返正跟LPC2478差不多)。放在老家了。
现在都不舍得花钱买新板子,等LPC2148玩得差不多了以后换STM32带外扩总线的吧。

jijuxie321 发表于 2009-7-28 21:37:28

补上HAL剩余的部分。。时间超过24小时不让编辑了。。|||

底层驱动该完成什么工作
定义好上层抽象接口和设备控制参数类型号,底层驱运应该按HAL的原理实现四个基本函数,就是设备控制块中所填写的三个函数指针指向的函数和一个设备初始化函数,和一个全局的计数信号量。由于这部分实际上是跟具体硬件相关,所以不在这里详细说明。编写驱动时需要工程师自行查阅数据手册实现相应功能。
例如UART0实现:
OS_EVENT* UART0_FILE;        //用于设备互斥管理的信号量
uint8 UART0Init(void);        //初始化UART0
uint8 UART0_SendNByte(uint8 *str, uint16 length);       
uint8 UART0_GetNByte(uint8 *str, uint16 length);
uint8 UART0_Control(void* param);

基中在uint8 UART0Init(void);函数的实现中要调用UC/OS-II的创建信号量函数初始化OS_EVENT* UART0_FILE。

其它外设编写底层驱动的方法类似。



头文件BSP.h中给出所有需要底层驱动完成的函数和信号量。添加新的外设的时候按照原有的文件组织形式进行函数,变量的声明和定义。确保将来方便系统移植。
#ifndef _BSP_H_
#define _BSP_H_

//#include "config.h"

#define OS_MEMSET(dest, c, count)        memset(dest, c, count)

void OpenTimerInter(void);//打开定时器操作函数由BSP根据硬件实现



extern OS_EVENT* UART0_FILE;
uint8 UART0Init(void);
uint8 UART0_SendNByte(uint8 *str, uint16 length);
uint8 UART0_GetNByte(uint8 *str, uint16 length);
uint8 UART0_Control(void* param);

extern OS_EVENT* UART1_FILE;
uint8 UART1Init(void);
uint8 UART1_SendNByte(uint8 *str, uint16 length);
uint8 UART1_GetNByte(uint8 *str, uint16 length);
uint8 UART1_Control(void* param);

extern OS_EVENT* I2C0_FILE;
uint8 I2C0Init(void);
uint8 I2C0_SendNByte(uint8* str,uint16 length);
uint8 I2C0_GetNByte(uint8* str,uint16 length);
uint8 I2C0_Control(void* param);


extern OS_EVENT* SPI0_FILE;
uint8 SPI0Init(void);
uint8 SPI0_SendNByte(uint8* str,uint16 length);
uint8 SPI0_GetNByte(uint8* str,uint16 length);
uint8 SPI0_Control(void *param);


extern OS_EVENT* SPI1_FILE;
uint8 SPI1Init(void);
uint8 SPI1_SendNByte(uint8* str,uint16 length);
uint8 SPI1_GetNByte(uint8* str,uint16 length);
uint8 SPI1_Control(void *param);




#endif

后续将介绍GPIO抽象方法,外部中断管理,GPIO的抽象独立于HAL。

aaa1982 发表于 2009-8-4 21:05:34

感谢,仔细的读一下

armecos 发表于 2009-8-4 21:38:08

armecos 发表于 2009-8-4 21:39:07

armecos 发表于 2009-8-4 21:40:08

armecos 发表于 2009-8-4 21:40:58

armecos 发表于 2009-8-4 21:42:12

armecos 发表于 2009-8-4 21:43:18

armecos 发表于 2009-8-4 21:44:52

armecos 发表于 2009-8-4 21:46:17

armecos 发表于 2009-8-4 21:51:03

kv2004 发表于 2009-8-4 21:53:04

好长,标记一下慢慢看

jchqxl 发表于 2009-8-4 22:36:09

LZ不错,谢谢

flight871 发表于 2009-8-5 10:01:46

标记,楼主好文章。
armecos的软件好贵,vxworks加ecos 不带BSP源码买1万多也太贵了吧,我自己改过一个44B0X的vxworks bsp包括网络驱动没花多少时间,smartarm2200没玩过但是也不会太难吧

wanas 发表于 2010-1-25 15:38:14

armecos 你占别的的帖子干吗?不能打个包再传上来啊。人家还没有讲完呢。

eduhf_123 发表于 2010-1-25 21:31:19

MARK HAL

GNMXD 发表于 2011-9-6 10:51:13

mark

mosongwen 发表于 2011-9-7 22:22:03

一口气地把它读完了,哈哈,长见识了,谢谢各位。

lan_boy008 发表于 2011-9-20 16:35:23

ddddddddddddd

HoldMyARM 发表于 2012-9-5 09:10:07

不错,顶了

SNOOKER 发表于 2012-11-3 20:06:20

armecos 发表于 2009-8-4 21:44 static/image/common/back.gif
GUI移植,通过虚拟桌面,你想移植什么GUI都可以。

**************************


"ecos增值包"有地方下载吗?

andrewpei 发表于 2013-1-6 08:58:08

这个留名看一下。

expressme 发表于 2013-1-30 09:18:53

好文,标记下!!HAL

lq1573 发表于 2013-2-23 13:59:52

这个留名看一下。

i55x 发表于 2013-3-12 18:59:18

精华好贴!
页: [1]
查看完整版本: 如何在MCU中实现一个基本的HAL?