|
昨天写了这个: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[MAX_DEV_MUN] = {
#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[MAX_DEV_MUN]){
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[8];
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);
}
占位:底层驱动要完成的工作。 |
阿莫论坛20周年了!感谢大家的支持与爱护!!
曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
|