在本节中我们来学习PX4中系统级驱动的设计原理与具体实现方法。PX4飞控程序架构非常灵活,它可以通过不同的编译选项,将飞控程序编译成不同平台运行的可执行程序。由于我们们采用的飞控板是Pixhawk系列,Pixhawk的主控芯片采用的是STM32F427VI,IO芯片使用的STM32F108C8。在编译时需要编译px4fmu-v3和px4io-v2选项,而这些在Pixhawk硬件上运行的程序都是基于开源嵌入式操作系统Nuttx的。PX4在编译时会将Nuttx操作系统与PX4飞控程序编译到一起,然后再烧写到STM32F427VI和STM32F103C8上。Nuttx是一款开源、类Unitx、优秀的嵌入式操作系统,关于Nuttx的相关知识不是我们本系列的重点,不做过多的说明,我们只来学习PX4飞控程序。由于在STM32系列上运行的飞控程序是运行在Nuttx操作系统上的,所以我们只关心操作系统上层的应用程序。我们在讲述飞控程序架构时已经简单的介绍了一些驱动模块,它们在程序中的位置在src/drivers/。这里存放的都是飞控中的驱动程序,驱动程序从级别上划分大致分为两种:
A.系统级驱动程序:在操作系统中注册设备节点/dev/xxx,并为应用级驱动程序提供标准的调用方法(open、close、read、write、seek、ioctl等)。
B.应用级驱动程序:通过操作现有的驱动程序设备节点,配置、读取、写入相关数据与设备节点交互,并通过uORB机制与上层应用进行交互,即将设备节点与上层应用建立通讯链路。
在本节中我们只学习系统级驱动程序的原理与实现,而应用级驱动程序将在下一节中来学习应用。
对于类Unix操作系统,尤其是Linux,或是类Linux操作系统来说,有一个非常重要的理念:一切皆文件。也就是说对于上层应用来说,操作系统所提供的所有内容,无论是何种内容,都是以“文件”形式存在的,它们对于使用者来说都是通用的,都是文件。无论是GPIO、串口、SPI、I2C或是USB接口,或是其它外设,它们对于上层应用都是文件,都存在于/dev/这个设备目录下,在这个设备目录下的所有内容,都被称为设备驱动节点,这些设备节点都是一些驱动程序在操作系统中注册的设备节点,它们对上层应用所暴露的接口都是以文件形式存在的。也就是说,上层应用程序可以像使用文件一样来使用这些驱动设备节点。操作系统为驱动程序提供了一个叫作struct file_operations的结构,这是操作系统定义的一个通用的文件操作结构体,内容其定义如下:
struct file_operations
{
int (*open)(FAR struct file *filep);
int (*close)(FAR struct file *filep);
ssize_t (*read)(FAR struct file *filep, FAR char *buffer, size_t buflen);
ssize_t (*write)(FAR struct file *filep, FAR const char *buffer, size_t buflen);
off_t (*seek)(FAR struct file *filep, off_t offset, int whence);
int (*ioctl)(FAR struct file *filep, int cmd, unsigned long arg);
};
这个结构体定义了标准、通用的文件操作函数接口(函数指针),驱动程序需要根据实际需要自行实现这些函数。同时驱动程序需要使用register_driver()函数向操作系统注册一个设备节点,我们来看一下register_driver()函数的定义:
int register_driver(FAR const char *path,
FAR const struct file_operations *fops,
mode_t mode,
FAR void *priv)
char *path:注册设备的节点路径,可以是任意的位置,但通常注册在/dev/目录下,例如:/dev/mpu6000、/dev/pwmout、/dev/ttyS0等。
struct file_operations *fops:即为上面讲述的文件操作函数指针结构,驱动程序需要实现它们。
mode_t mode:设备节点的操作权限,通常是0666。格式为 类型 / 所属者 / 所属组 / 其它。除类型为-和d之外,其它权限均为0~7,分别表示rxw权限。
void *priv:允许驱动程序根据需要存放自定义的数据内容。
由于PX4是采用C和C++混合编程的开源飞控程序,所以使用C语言编程的用户可以直接使用register_driver()函数来向操作系统注册一个驱动设备节点,而使用C++语言编程的用户可以定义一个驱动程序类并继承CDev类,来注册设备节点。CDev类是PX4程序中提供的一个C++基类,驱动程序需要继承,并重写open、close、read、write、ioctl、seek等方法。这与C语言函数中直接调用register_driver()函数没有任何区别,只不过在使用形式上有所不同罢了,读者可以根据自己的习惯去选择用哪一种方法来编写驱动程序而不必拘泥于实现方式。
接下来我们就来实现一个Led灯的驱动程序。
1.配置编译选项:在cmake/configs/nuttx_px4fmu-v3_default.cmake中config_module_list列表中加入Led驱动的编译选项:
2.创建驱动程序:在src/drivers/目录下创建一个叫led的文件夹src/drivers/led,并在基中创建两个文件CMakeLists.txt和led.c(这里作者是采用C语言来编写的驱动程序,如果使用C++的话需要创建led.cpp并继承CDev类,不再赘述):
CMakeLists.txt为此驱动模块的编译选项,内容如下:
px4_add_module(
MODULE drivers__led #模块名称
MAIN led #入口函数 int led_main(int argc, char *argv[])
STACK_MAIN 2000 #栈大小
SRCS #源代码文件
led.c
DEPENDS #依赖模块
platforms__common
)
3.配置文件操作函数:在led.c文件中定义open、close、read、write、seek、ioctl的接口函数和驱动文件操作符struct file_operations:
static int led_open(FAR struct file *filep);
static int led_close(FAR struct file *filep);
static ssize_t led_read(FAR struct file *filep, FAR char *buffer, size_t buflen);
static ssize_t led_write(FAR struct file *filep, FAR const char *buffer, size_t buflen);
static off_t led_seek(FAR struct file *filep, off_t offset, int whence);
static int led_ioctl(FAR struct file *filep, int cmd, unsigned long arg);
static struct file_operations _fops =
{
.open = led_open,
.close = led_close,
.read = led_read,
.write = led_write,
.seek = led_seek,
.ioctl = led_ioctl
};
这样我们就将此Led的驱动文件操作的open、close、read、write、seek、ioctl函数接口定义完成了,当上层应用调用open()函数时,实际上是调用的led_open()函数,调用ioctl()函数时,实际上是调用的led_ioctl()函数,其它函数原理都一样。
4.实现驱动函数:这6个led驱动函数代码如下:
#define CMD_LED_ON 1
#define CMD_LED_OFF 0
int led_open(FAR struct file *filep)
{
stm32_configgpio(GPIO_LED1);
return 0;
}
int led_close(FAR struct file *filep)
{
return 0;
}
ssize_t led_read(FAR struct file *filep, FAR char *buffer, size_t buflen)
{
return 0;
}
ssize_t led_write(FAR struct file *filep, FAR const char *buffer, size_t buflen)
{
return 0;
}
off_t led_seek(FAR struct file *filep, off_t offset, int whence)
{
return 0;
}
int led_ioctl(FAR struct file *filep, int cmd, unsigned long arg)
{
switch(cmd)
{
case CMD_LED_ON:
{
stm32_gpiowrite(GPIO_LED1, false);
break;
}
case CMD_LED_OFF:
{
stm32_gpiowrite(GPIO_LED1, true);
break;
}
default:
{
break;
}
}
return 0;
}
其中需要说明的是led_open()和led_ioctl()这个函数,我们知道STM32中在使用GPIO管脚之前需要对其做初始化配置,所以当用户调用led_open()函数时,我们需要对Led的管脚做初始化配置,对于STM32的GPIO配置,只需调用stm32_configgpio(GPIO_LED1);即可,而GPIO_LED1定义的位置在src/drivers/boards/px4fmu-v2/board_config.h,内容为:
#define GPIO_LED1 (GPIO_OUTPUT| \
GPIO_OPENDRAIN| \
GPIO_SPEED_50MHz| \
GPIO_OUTPUT_CLEAR| \
GPIO_PORTE| \
GPIO_PIN12)
而对于led_ioctl()函数我们需要接收两个命令LED_ON和LED_OFF,当上层应用程序调用ioctl()时需要传入一个int cmd参数,表示希望使Led开或者关,所以在led_ioctl()中通过switch(cmd)来判断参数的值与LED_ON或LED_OFF是否相等,然后通过调用stm32_gpiowrite()函数来将GPIO管脚拉高或拉低,从而使得Led的灯开或关。
5.实现入口函数:led_main():
int led_main(int argc, char *argv[]);
int led_main(int argc, char *argv[])
{
if (argc <= 1)
{
return -1;
}
if (strcmp(argv[1], "start") == 0)
{
if (register_driver("/dev/led", &_fops, 0666, NULL) < 0)
{
printf("led reg err.\n");
return -1;
}
}
if (strcmp(argv[1], "stop") == 0)
{
unregister_driver("/dev/led");
}
if (strcmp(argv[1], "on") == 0)
{
int fd = open("/dev/led", O_RDWR);
if (fd < 0)
{
printf("could not open /dev/led.\n");
return -1;
}
ioctl(fd, CMD_LED_ON, 0);
close(fd);
return 0;
}
if (strcmp(argv[1], "off") == 0)
{
int fd = open("/dev/led", O_RDWR);
if (fd < 0)
{
printf("could not open /dev/led.\n");
return -1;
}
ioctl(fd, CMD_LED_OFF, 0);
close(fd);
return 0;
}
if (strcmp(argv[1], "blink") == 0)
{
int fd = open("/dev/led", O_RDWR);
if (fd < 0)
{
printf("could not open /dev/led.\n");
return -1;
}
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
ioctl(fd, CMD_LED_ON, 0);
}
else
{
ioctl(fd, CMD_LED_OFF, 0);
}
sleep(1);
}
close(fd);
return 0;
}
return -1;
}
我们通过判断主函数中argv[1]的参数值(start / stop / on / off / blink)来分别执行不同的功能:
start:执行register_driver("/dev/led", &_fops, 0666, NULL)函数来注册/dev/led设备节点。
stop:执行unregister_driver("/dev/led");函数来取消注册/dev/led设备节点。
on:打开设备节点,并对其调用ioctl()函数,使Led开,然后关闭设备节点。 ——非驱动程序,上层应用测试。
off:打开设备节点,并对其调用ioctl()函数,使Led关,然后关闭设备节点。 ——非驱动程序,上层应用测试。
off:打开设备节点,并对其调用ioctl()函数,使Led关,然后关闭设备节点。 ——非驱动程序,上层应用测试。
blink:打开设备节点,并对其调用ioctl()函数,使Led每秒闪一次,共10秒,然后关闭设备节点。 ——非驱动程序,上层应用测试。
6.测试驱动程序:编译程序并烧写到飞控程序中,通过调试口进入nsh终端,执行 led start 回车,就可以看到 /dev/led 的驱动节点:
执行 led on 或 led off 或 led blink即可看到Led灯的开关效果了。
Copyright © 2015-2023 问渠网 辽ICP备15013245号