这几天看了一下Linux设备驱动,发现这套思想其实也可以用在普通的单片机编程上。这种思想较好的分割了驱动层和应用层的任务,方便分层开发。
以前,我们开发STM32驱动的时候,会给设备写一套函数来控制它。假设现在有一块单片机开发板,外接设备UART和LCD。
我们会这样写函数
--------UART驱动------
void UART_Init();
int UART_Send(const char *str,int size);
int UART_Rec(char *rec_buffer);
...
--------LCD驱动-------
void LCD_Init();
void LCD_DrawPoint(int x0,int y0);
void LCD_DrawPicture(picure* mypicture);
...
然后我们拿这些函数进行应用开发。
这样做并不是不可以,但不同的设备,其函数名、形参都不一致。这导致做纯应用层开发的人,无法快速理解和使用设备。要么他需要经常去咨询驱动层的工程师,要么干脆应用和驱动全由一个人负责。
当引用Linux驱动设备的思想之后,这个问题可以得到解决。这种思想大概就是,不管驱动层实际控制的是什么设备,应用层只需要使用open,read,write,close这样的函数就能操作设备。这些函数原型都是广为人知的C语言文件操作函数。
首先定义一个结构体,名为Device,它包含了设备名等一些必要信息,和对设备的四个操作:open,read,write,close。这些操作由函数指针实现。
typedef struct device
{
/* data */
char device_name[10];
int (*open)(const char * pathname, int flags);
int (*read)(int fd, void * buf, int count);
int (*write)(int fd, void * buf, int count);
int (*close)(int fd);
}device;
这个device就可以实例化为各种具体的设备。比如一个UART,我可以定义device uart;然后设置uart的device_name为"uart"。
然后定义uart_open函数,在里面进行UART的初始化。
int uart_open(const char * pathname, int flags)
{
/*
UART引脚初始化;
UART时钟初始化;
UART波特率设置....
*/
}
同理,定义uart_read,uart_write,uart_close。uart_read的功能可为从UART判断标志位并接收数据,uart_write的功能可为UART发送数据。uart_close可以为关闭UART时钟。
定义好了之后,把uart的函数指针赋予初始值就行。
device uart =
{
"uart",
uart_open,
uart_read,
uart_write,
uart_close
};
这样,一个活生生的设备就体现出来了。现在最重要的是uart这个结构体变量。只要拿到了这个变量,就可以控制UART。因为这里面有UART的基本信息和操作它需要用到的函数。
我们还可以定义device lcd,把它也进行赋值,操作过程和uart一致。
系统中有很多设备uart,lcd,led,button....都可以像这样定义device xxx。为了方便管理,做一个数组(或者链表等其他线性表),把所有的device变量组织在一起,取名设备管理表。
typedef struct device_list
{
device *Device_List[DEVICE_LIST_NUM];
int Device_List_index;
}device_list;
device_list global_device_list ;
把设备加入这张设备管理表的操作,就叫设备注册。
void Device_register(device* mydevice)
{
int index = global_device_list.Device_List_index;
global_device_list.Device_List[index++] = mydevice;
global_device_list.Device_List_index = index;
}
所有的设备都注册好之后,以后需要使用哪个设备xxx,可以凭设备名device_name在这张表中搜索,最后返回搜索结果。
int Device_find(const char* device_name)
{
int i = 0;
int index = global_device_list.Device_List_index;
for(i=0;i
if(strcmp(global_device_list.Device_List[i]->device_name,device_name)==0)
{
return i;
}
}
return DEVICE_LIST_NUM;
}
拿到了搜索结果,意味着拿到了device xxx这个结构体变量,就可以通过它的结构体成员open,read,write,close来控制设备了。岂不是很方便?要注意的是,read,write的函数形参里面有void *类型;这意味着你其实可以传入任何类型的数据指针,而不仅仅是传统意义上的字符串。这很有用,因为不同的设备需要进行交互的信息可能不一样,单纯字符串想要较好的表示这些信息有难度。
int Device_open(const char * pathname, int flags)
{
device* dev;
int fd = Device_find(pathname);
dev = global_device_list.Device_List[fd];
(* dev).open( pathname, flags);
return fd;
}
int Device_read(int fd, void * buf, int count)
{
device* dev;
dev = global_device_list.Device_List[fd];
return (*dev).read( fd, buf, count);
}
int Device_write(int fd, void * buf, int count)
{
device* dev;
dev = global_device_list.Device_List[fd];
return (*dev).write( fd, buf, count);
}
int Device_close(int fd )
{
device* dev;
dev = global_device_list.Device_List[fd];
return (*dev).close( fd );
}
使用举例:
void main()
{
int fd = 0;
/*先注册*/
Device_register(&uart);
Device_register(&lcd);
/*后使用*/
fd = Device_open("uart",0);
Device_read(fd,NULL,0);
Device_write(fd,"*****i am uart***\n",12);
Device_close(fd);
printf("fd = %d \n",fd);
fd = Device_open("lcd",0);
Device_read(fd,NULL,0);
Device_write(fd,"*****i am lcd****\n",12);
Device_close(fd);
printf("fd = %d \n",fd);
}
这样做之后,应用层开发人员无须在各种不同的驱动API中焦头烂额。他只需要知道两点:
1.设备名。"uart","lcd","led"...
2.read和write的数据格式。
这就够了。open,read,write,close这些都是市民皆知的函数。无论什么设备,接口都能保持一致。相当完美的分离了应用层和驱动层的开发。由于不上文件系统,所以原有的open,read,write,close以Device_open,Device_read,Device_write,Device_close替代。
当然,其实文件操作的函数也不光有这四个,还有seek等等。感兴趣可以加入到device结构体里面。但要注意,定义的函数指针越多,应用开发的负担越重。在够用的情况下,尽量少定义一些没用的成员是明智之举。