Nuttx系统学习笔记(三)——使用Nuttx操作STM32F429外设

在上一篇,我们已经学会了如何将Nuttx进行烧录,以及学会了如何部署这个操作系统,接下来我们就要使用这个操作系统来实现我们对嵌入式设备的控制,当然也是从点灯开始的。这个基于Posix架构的操作系统使用起来是跟FreeRTOS那些操作系统是有区别的,所以首先我先补充一下这个操作系统的一些需要注意的地方:

目录

0x01 关于这个系统的一些补充

0x02 底层驱动解读

(一)GPIO.h

0x03 Nuttx操作系统的驱动编写

(一)character-index

(二)block-index

(三)special-index

(四)注册驱动函数以及解除注册驱动函数

0x04 编写LED驱动

(一)思路

(二)驱动框架搭建

0x05 使用已经封装好的驱动来点灯

(一)修改库文件


0x01 关于这个系统的一些补充

去研读这个操作系统的文档可以发现这个操作系统有两种内核构建模式,一种是Kernel构建,一种是flat构建,当然这两种构建模式下所存在的内存管理制度是不一样的,内核构建与Linux更贴切,它具有内存管理单元(MMU),虽然说它最后是会映射在物理内存上,但是它可以使用虚拟内存这个东西运行比实际内存更大的东西,并且只有在内核模式下才可以执行进程这个概念,当然,在内核构建模式下,在进程结束后,它会回收该进程的资源以及空间,所以你可以不需要free来释放内存,也不会导致内存泄漏。但是对于flat构建,它是在偏上内存之外的未受保护的平面地址空间中运行的,flat构建是没有MMU来进行构建的,进程退出时并没有自动清除的功能,所以要手动free,否则会导致内存泄漏。

如果使用内核构建模式,则需要MMU来进行管理,与Linux构建的方式类似。对于ARM的内存管理对于虚拟地址的内存分区(4GIB),一共分为3GIB用户空间以及1GIB的内核空间分区。对于这4GIB的内存分区,以及一级页表将其分为4096个条目,每个条目1MByte;二级页表将这1MByte再细分成256个条目,每个条目4KByte ,这个4K也是系统中最小的页面,这样可以提高内存的使用效率。

在低端的嵌入式设备(cortex-m)里程序直接运行在物理内存上。在高端的嵌入式设备(cortex-A)里最终操控的也是物理内存,但是由于有MMU这个硬件,所以有了虚拟内存这个概念,MMU实现了虚拟内存到物理内存的映射关系。

那么如何配置这些内存模式呢,可以通过设置下面的宏来定义:

  • CONFIG_PAGING(按需分页,也是一种虚拟内存的管理方法,需要MMU)

  • CONFIG_BUILD_PROTECTED(保护构建,不需要使用MMU来支持保护版本,地址空间为平面,不会进行地址映射)

  • CONFIG_ARCH_ADDRENV(启动每个程序时,都会为新任务创建一个新的地址环境,需要MMU)*

  • CONFIG_BUILD_KERNEL(类似于Linux,可以使用进程,具有内核区域以及用户区域)

来使其进入不同的模式,其中CONFIG_BUILD_KERNEL=y是使得Nuttx中支持使用进程的必要条件,只有内核构建才支持进程模式。

这个操作系统的教程入口:https://www.youtube.com/channel/UC0QciIlcUnjJkL5yJJBmluw

0x02 底层驱动解读

在这里我是使用的STM32F429IGT6的开发板,当然这个开发板是在它的支持list中是没有的,所以我选择了mikroe-stm32f4:nsh来进行配置,可以使用如下指令快速查找:

./tools/configure.sh -L | less | grep "stm32f4"

之后可以得到下面这些开发板型号,可以看到下面这些配置:

 这里只有几块开发板可以选择:stm32f411e-discomikroe-stm32f4mikroe-stm32f4。之后就去网上查资料了,看看哪一块跟我这块开发板上的芯片是一样的,最后决定使用stm32f429i-disco试一下了:

./tools/configure.sh -l  stm32f429i-disco:nsh
make menuconfig
在菜单中进行如下配置,当然这些都是那块stm32f429i-disco上的东西,我们需要到底层改一下底层驱动编号,或者直接使用gpio操作也是可以的。

 

打开后保存一下,然后再make,然后打开codeblocks IDE,把我们nuttx系统中生成的的头文件以及源文件包含进去,为什么这么干呢,因为这样可视化程度比较高,没别的意思。。

毕竟不是自己手头上这块开发板的型号,也不是这些外设,所以我们需要看懂它的底层驱动文件,然后在文件中改他的引脚号即可。所以我们是直接操作gpio,而不是直接使用它的examples的例程,当然也可以花时间去改,但是还是走最近的路线吧:

(一)GPIO.h

在nuttx的库中找到gpio.h的库,找到之后打开研究一下这个文件,最后发现这是GPIO的一些底层实现的驱动文件,在里面我们可以看到在这里定义了GPIO的几种类型:

#define GPIOC_WRITE      _GPIOC(1)
#define GPIOC_READ       _GPIOC(2)
#define GPIOC_PINTYPE    _GPIOC(3)
#define GPIOC_REGISTER   _GPIOC(4)
#define GPIOC_UNREGISTER _GPIOC(5)
#define GPIOC_SETPINTYPE _GPIOC(6)
  • GPIOC_WRITE:这个其实在单片机中,我们可以看做是GPIO电平的输出,可以给0和1,返回值:0=output a low value; 1=output a high value

  • GPIOC_READ:这个在单片机中就是输入的状态,读取GPIO引脚的电平,可以看到返回值:false=low value; true=high value

  • GPIOC_PINTYPE:返回GPIO的类型,具体是什么类型可以看下面的结构体定义:

enum gpio_pintype_e
{
  GPIO_INPUT_PIN = 0, /* float */
  GPIO_INPUT_PIN_PULLUP,
  GPIO_INPUT_PIN_PULLDOWN,
  GPIO_OUTPUT_PIN, /* push-pull */
  GPIO_OUTPUT_PIN_OPENDRAIN,
  GPIO_INTERRUPT_PIN,
  GPIO_INTERRUPT_HIGH_PIN,
  GPIO_INTERRUPT_LOW_PIN,
  GPIO_INTERRUPT_RISING_PIN,
  GPIO_INTERRUPT_FALLING_PIN,
  GPIO_INTERRUPT_BOTH_PIN,
  GPIO_NPINTYPES
};
  • 这个结构体是与驱动程序:/ioexpander/gpio_lower_half.c是相互对应的,所以当修改了这个结构体的时候需要修改那个文件的结构体,要相互匹配。

  • GPIOC_REGISTER:在有中断的情况下,接收GPIO的电平状态变化,说白了就是一个外部中断,检测电平变化,这个功能更多取决于平台对中断GPIO的支持。中断时要根据信号类型来做不同的反应。应该是跟信号返回值是有关的。

  • GPIOC_UNREGISTER:停止接收信号。

  • GPIOC_SETPINTYPE:可以设定GPIO的引脚类型,也就是上面那个枚举所列出来的。

接下来是中断的回调函数,学过单片机开发的应该都知道,当检测到中断信号的发生时,我们需要到回调函数中进行实现相关的操作,那么在这里,它声明了一个函数指针,所以当我们在声明中断函数的时候,需要将这个指针指向某个跟他有一样形参的函数:

struct gpio_dev_s;
typedef CODE int (*pin_interrupt_t)(FAR struct gpio_dev_s *dev, uint8_t pin);

所以在上层驱动中,我们需要传入一个gpio的结构体以及对应的引脚号进行操作。在这里补充一个点:FAR标识符,表示的是远地址,只能修饰于函数、全局变量和指针变量,对于非指针类型的局部变量,这些关键字没有实际意义。那么对于上面修饰的指针,表示的是可以寻址1MB地址空间的任意一个地方,far型指针的实质是一个32bit的整型数,高16bits为段值,低16bits为段内偏移,这个关键字受目标嵌入式设备体系结构的影响。

在这里只是声明了一个函数的框架,具体的使用可以看看这个芯片的gpio的封装,就可以一目了然了:

可以看到前面声明的那个结构体,当他传入这个结构体的指针时,它可以取到struct gpio_dev_s的首地址,所以形参就可以对上了。

在往下看,可以看到一些gpio的操作:

struct gpio_dev_s;
struct gpio_operations_s
{
  /* Interface methods */

  CODE int (*go_read)(FAR struct gpio_dev_s *dev, FAR bool *value);
  CODE int (*go_write)(FAR struct gpio_dev_s *dev, bool value);
  CODE int (*go_attach)(FAR struct gpio_dev_s *dev,
                        pin_interrupt_t callback);
  CODE int (*go_enable)(FAR struct gpio_dev_s *dev, bool enable);
  CODE int (*go_setpintype)(FAR struct gpio_dev_s *dev,
                            enum gpio_pintype_e pintype);
};
  • go_read:所有GPIO都需要的操作。

  • go_write:仅GPIO_OUTPUT_PIN引脚类型是必需的。 不用于其他引脚的类型,可能为 NULL。

  • go_attach go_enable:仅对GPIO_INTERRUPT_PIN引脚类型是必需的。 不用于其他引脚类型,可能为 NULL。

  • go_setpintype:所有GPIO都需要的操作。

0x03 Nuttx操作系统的驱动编写

驱动就是让硬件(也就是设备)正常工作,并将设备的数据接入OS中,以便于应用开发所用的程序。不同的设备有不同的驱动,原因在于设备的型号、作用、适用条件,通信接口都不尽相同,所以我们需要封装好下层驱动,上层驱动才可以统一函数来进行实现,作为一个中间件,乘上服务于应用程序,下面作用于硬件设备。

这个Nuttx的驱动分为三大类:字符驱动(PWM\CAN\TIMER等)、块设备驱动、特殊驱动(IIC\SPI\LCD\USB等)。可以在路径/nuttx/Documentation/components/drivers下看到相关的驱动文件描述,也就是那些后缀为.rst格式的文件。对于每个驱动文件,我们都得先看看他的一些描述,查看他们的index.rst文件。

(一)character-index

那么如何写驱动呢,Nuttx操作系统并没有提供像Linux系统那样复杂的驱动模型机制,比如Device、Driver、Bus、Class等。Nuttx只是简单的通过驱动注册接口,将驱动注册进文件系统中,并实现file_operation操作函数集。上层应用便能通过标准的系统调用,进而调用到低层的驱动,而底层的驱动又分为了上半部分(upper_half)和下半部分(lower_half)。

/* This structure is provided by devices when they are registered with the
 * system.  It is used to call back to perform device specific operations.
 */

struct file_operations
{
  /* The device driver open method differs from the mountpoint open method */

  int     (*open)(FAR struct file *filep);

  /* The following methods must be identical in signature and position
   * because the struct file_operations and struct mountpt_operations are
   * treated like unions.
   */

  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);

  /* The two structures need not be common after this point */

  int     (*poll)(FAR struct file *filep, struct pollfd *fds, bool setup);
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  int     (*unlink)(FAR struct inode *inode);
#endif
};

这个定义于:include/nuttx/fs/fs.h,这个文件夹中是字符驱动程序文件所需的所有结构和API。这个结构体中是使用函数集,也就是底层驱动来实现的。这个函数提供了设备的注册,并且可以通过这几个函数去操作这个设备,一切皆是文件的思想。

它的注册是使用:int register_driver(const char *path, const struct file_operations *fops, mode_t mode, void *priv);这个函数来进行注册,需要传递它的路径,这个路径是出现在ref也就是伪文件系统中,它是结构file_operations的初始化实例。

整哈书首先是根据path,比如我们的/dev/xxx来查找是否有对应的inode,如果没有的话,那就为path创建一个inode。然后将实际驱动实现的struct file_operations更新到path对应的inode中,此外还设置了权限。之后将priv数据设置仅inode中,这个一般存放驱动的私有数据。注册后,我们就可以使用标准驱动程序的用户代码访问驱动设备,比如可以使用这些函数:open(), close(), read(), write()

那么,对于其使用,比如我们要操作一个设备,第一步就是要打开相应的设备文件,即使用open()打开设备,其返回一个文件描述符fd。打开设备号之后对该设备的操作可以通过fd来完成。应用中以open()以设备的节点路径和操作权限为参数,操作进入VFS(虚拟文件系统),调用fs_open.c中的open()函数,通过设备路径找到对应的inode节点,在进程的文件描述符链表中寻找并分配空闲可用的描述符fd和文件file,最后调用设备节点inode中的文件操作符file_operation中的函数open()。应用程序调用成功时,返回本次分配的文件描述符fd,发生错误时,返回-1,错误码记录在errno中。

对于close()函数,它是用关闭设备文件,释放设备文件、文件描述符fd

对于read()函数,它是从设备读取数据,它需要三个参数,一个是文件指针,一个是数据buffer,一个是读取buffer的长度。write()函数是往设备中写数据。

对于seek()函数,它是用来查找或调整文件读写位置。

对于ioctl()函数,可以用于执行设备特定命令,如设置设备属性、配置设备寄存器等。

对于poll()函数,用于查询指定的一组文件是否可读或可写。首先初始化信号量,用于实现阻塞,直到文件可读或可写(也可以设置超时时间)。如果文件可读写,那么他会修改对应的pollfd中的返回事件标志为对应的事件;并且释放信号量。这个函数有三个参数:poll_fd数组指针,查询的文件数量,等待时间。返回正数:表示可读写的文件数量,返回0表示超时,返回-1表示错误。

这里面也有专用字符串驱动程序,有些字符驱动程序有着自己独特的字符驱动要求,这些不同于其他字符串的形式往往使用以下来进行实现:

  • 用于执行的设备特定ioctl()命令操作一些专门的设备,可以使用这个命令的,在include/nuttx下的头文件中有这个接口的详细说明:


/****************************************************************************
 * Name: ioctl
 *
 * Description:
 *   Perform device specific operations.
 *
 *   ioctl() is a non-standard UNIX-like API
 *
 * Input Parameters:
 *   fd       File/socket descriptor of device
 *   req      The ioctl command
 *   arg      The argument of the ioctl cmd, OR
 *   ...      A third argument of type unsigned long is still expected.
 *
 * Returned Value:
 *   >=0 on success (positive non-zero values are cmd-specific)
 *   -1 on failure with errno set properly:
 *
 *   EBADF
 *     'fd' is not a valid descriptor.
 *   EFAULT
 *     'arg' references an inaccessible memory area.
 *   EINVAL
 *     'cmd' or 'arg' is not valid.
 *   ENOTTY
 *     'fd' is not associated with a character special device.
 *   ENOTTY
 *      The specified request does not apply to the kind of object that the
 *      descriptor 'fd' references.
 *
 ****************************************************************************/

int ioctl(int fd, int req, ...);
  • 这个接口主要用于设备输入输出操作的系统调用,该调用传入了一个跟设备有关的请求码,系统调用的功能完全取决于请求码。

  • 专用的I/O模式。某些设备需要read()write()的操作来使一些数据传输到特定的格式,而不是普通的字节流,这个也是在include/nuttx下的头文件中有这两个接口的详细说明。

对于以上两个专用的字符驱动程序都是记录在一下的程序中:drivers/dev_null.c, drivers/fifo.c,drivers/serial.c,等等。

最后总结了这个字符驱动程序支持的设备: serial.rst

  touchscreen.rst
  analog.rst
  pwm.rst
  can.rst
  quadrature.rst
  timer.rst
  rtc.rst
  watchdog.rst
  keypad.rst
  note.rst
  foc.rst
  ws2812.rst

(二)block-index

这个块设备驱动也是有基于nclude/nuttx/fs/fs.h的一些操作,提供了一些结构体以及驱动API。具体可以看看下面的这个结构体:

struct inode;
struct block_operations
{
  int     (*open)(FAR struct inode *inode);
  int     (*close)(FAR struct inode *inode);
  ssize_t (*read)(FAR struct inode *inode, FAR unsigned char *buffer,
            blkcnt_t start_sector, unsigned int nsectors);
  ssize_t (*write)(FAR struct inode *inode, FAR const unsigned char *buffer,
            blkcnt_t start_sector, unsigned int nsectors);
  int     (*geometry)(FAR struct inode *inode, FAR struct geometry
                      *geometry);
  int     (*ioctl)(FAR struct inode *inode, int cmd, unsigned long arg);
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  int     (*unlink)(FAR struct inode *inode);
#endif
};

这个结构体由块设备向系统注册时提供,文件系统使用它来执行文件系统传输每个设备节点的位置与数据,下面是它主要的函数接口:

  • int register_blockdriver(const char *path, const struct block_operations *bops, mode_t mode, void *priv);每个驱动程序都是通过调用这个函数来进行注册自己,并向其传递路径(出现在伪文件系统的路径),并且创建一个block实例。

  • 用户通常不直接访问块驱动程序,而是通过间接访问块驱动程序mount()来绑定块驱动程序,这个API具有文件系统和挂载点。然后用户可以使用块驱动程序访问文件系统上的底层媒体。可以于cmd_mount()查看实现,是在apps/nshlib/nsh_fscmds.c中实现的。

  • 也可以将字符驱动程序作为块设备访问,可以参阅drivers/loop.c

对于这个block的使用可以看下面这些文件:drivers/loop.c,drivers/mmcsd/mmcsd_spi.c, drivers/ramdisk.c

(三)special-index

对于nuttx系统的上半部分以及下半部分,使用诸如register_driver或者是register_blockdriver等调用自身注册到nuttx的上半部分,并实现相应的高级接口(read、write、driver等)。通过这些接口,可以通过上半部分调用下半部分。

(四)注册驱动函数以及解除注册驱动函数

int register_driver(FAR const char *path,
                    FAR const struct file_operations *fops,
                    mode_t mode, FAR void *priv)
{
  FAR struct inode *node;
  int ret;

  /* Insert a dummy node -- we need to hold the inode semaphore because we
   * will have a momentarily bad structure.
   */

  ret = inode_lock();
  if (ret < 0)
    {
      return ret;
    }

  ret = inode_reserve(path, mode, &node);
  if (ret >= 0)
    {
      /* We have it, now populate it with driver specific information.
       * NOTE that the initial reference count on the new inode is zero.
       */

      INODE_SET_DRIVER(node);

      node->u.i_ops   = fops;
      node->i_private = priv;
      ret             = OK;
    }

  inode_unlock();
  return ret;
}

对于参数一:设置设备路径,例如注册一个key驱动到/dev/key

对于参数二:设备的文件操作指针,指向文件操作实例。

对于参数三:预算的设备访问权限。

对于参数四:为设备驱动传递的私有参数。

返回值:0代表注册成功,返回负数,表示注册失败。

对于里面的一个函数:INODE_SET_DRIVER,底层的实现是这样的:

#define INODE_SET_TYPE(i,t) \
  do \
    { \
      (i)->i_flags = ((i)->i_flags & ~FSNODEFLAG_TYPE_MASK) | (t); \
    } \
  while (0)

#define INODE_SET_DRIVER(i)   INODE_SET_TYPE(i,FSNODEFLAG_TYPE_DRIVER)

而对于宏定义FSNODEFLAG_TYPE_DRIVER

#define   FSNODEFLAG_TYPE_DRIVER    0x00000001 /*   Character driver       */
#define   FSNODEFLAG_TYPE_BLOCK     0x00000002 /*   Block driver           */
#define   FSNODEFLAG_TYPE_MOUNTPT   0x00000003 /*   Mount point            */
#define   FSNODEFLAG_TYPE_MASK        0x0000000f /* Isolates type field      */

我猜这个是把这个node的模式配置写到某个寄存器中存储。

对于删除注册函数:

int unregister_blockdriver(FAR const char *path)
{
  int ret;

  ret = inode_lock();
  if (ret >= 0)
    {
      ret = inode_remove(path);
      inode_unlock();
    }

  return ret;
}

其实现是与Linux是相似的。最后补充两个重要的文件描述结构体以及节点的结构体:

struct inode
{
  FAR struct inode *i_parent;   /* Link to parent level inode */
  FAR struct inode *i_peer;     /* Link to same level inode */
  FAR struct inode *i_child;    /* Link to lower level inode */
  int16_t           i_crefs;    /* References to inode */
  uint16_t          i_flags;    /* Flags for inode */
  union inode_ops_u u;          /* Inode operations */
  ino_t             i_ino;      /* Inode serial number */
#ifdef CONFIG_PSEUDOFS_ATTRIBUTES
  mode_t            i_mode;     /* Access mode flags */
  uid_t             i_owner;    /* Owner */
  gid_t             i_group;    /* Group */
  struct timespec   i_atime;    /* Time of last access */
  struct timespec   i_mtime;    /* Time of last modification */
  struct timespec   i_ctime;    /* Time of last status change */
#endif
  FAR void         *i_private;  /* Per inode driver private data */
  char              i_name[1];  /* Name of inode (variable) */
};

struct file
{
  int               f_oflags;   /* Open mode flags */
  off_t             f_pos;      /* File position */
  FAR struct inode *f_inode;    /* Driver or file system interface */
  FAR void         *f_priv;     /* Per file driver private data */
};

0x04 编写LED驱动

(一)思路

在开发板上有一个RGB的灯,把他作为驱动写进/dev中,这里就先不用PWM控制了。那么从上面的分析,我们可以按照如下的流程来编写:

  • 创建字符串设备驱动主体,即文件操作file_operations,然后实现open()close()read()write()ioctl()

  • 注册该设备驱动到系统中,将会在系统的/dev目录下生成一个名为LEDBling的设备节点。

  • 通过应用程序来获取输入脚状态、控制输出脚状态。

那么首先我们先研究一下这个库的例程是怎么写的吧,并且我们也需要到底层改一下自己的引脚号,绑定自己的引脚编号,首先我们需要去board文件夹中找到自己配置时的那个型号,我的路径是这样的:

/home/zhengxiting/nuttxspace/nuttx/boards/arm/stm32/stm32f429i-disco

那么找到这个文件:board.h,之后就可以看到很多的STM32熟悉的外设接口了。首先我们先看看例程的程序/home/zhengxiting/nuttxspace/apps/examples/gpio,首先先看懂这个程序吧:

首先包含的头文件有这么多:

#include <nuttx/config.h>

#include <sys/ioctl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>

#include <nuttx/ioexpander/gpio.h>

这里看到它包含了上面说的gpio.h底层文件,接下来看看主程序:

首先是定义了一个指向/dev设备的指针,接下来是定义了gpio描述枚举,接下来就是各种定义标志位、还有文件描述符、检查返回值等各种变量:

  FAR char *devpath = NULL;
  enum gpio_pintype_e pintype;
  enum gpio_pintype_e newpintype;
  bool havenewtype = false;
  bool havesigno = false;
  bool invalue;
  bool outvalue = false;
  bool haveout = false;
  int signo = 0;
  int ndx;
  int ret;
  int fd;

这个程序主要实现的是通过输入参数,对GPIO进行输入输出的配置,配置的内容有如下:

static void show_usage(FAR const char *progname)
{
  fprintf(stderr, "USAGE: %s [-t <pintype>] [-w <signo>] [-o <value>] "
          "<driver-path>\n", progname);
  fprintf(stderr, "       %s -h\n", progname);
  fprintf(stderr, "Where:\n");
  fprintf(stderr, "\t<driver-path>: The full path to the GPIO pin "
          "driver.\n");
  fprintf(stderr, "\t-t <pintype>:  Change the pin to this pintype "
          "(0-10):\n");
  fprintf(stderr, "\t-w <signo>:    Wait for a signal if this is an "
          "interrupt pin.\n");
  fprintf(stderr, "\t-o <value>:    Write this value (0 or 1) if this is an "
          "output pin.\n");
  fprintf(stderr, "\t-h: Print this usage information and exit.\n");
  fprintf(stderr, "Pintypes:\n");
  fprintf(stderr, "\t 0: GPIO_INPUT_PIN\n");
  fprintf(stderr, "\t 1: GPIO_INPUT_PIN_PULLUP\n");
  fprintf(stderr, "\t 2: GPIO_INPUT_PIN_PULLDOWN\n");
  fprintf(stderr, "\t 3: GPIO_OUTPUT_PIN\n");
  fprintf(stderr, "\t 4: GPIO_OUTPUT_PIN_OPENDRAIN\n");
  fprintf(stderr, "\t 5: GPIO_INTERRUPT_PIN\n");
  fprintf(stderr, "\t 6: GPIO_INTERRUPT_HIGH_PIN\n");
  fprintf(stderr, "\t 7: GPIO_INTERRUPT_LOW_PIN\n");
  fprintf(stderr, "\t 8: GPIO_INTERRUPT_RISING_PIN\n");
  fprintf(stderr, "\t 9: GPIO_INTERRUPT_FALLING_PIN\n");
  fprintf(stderr, "\t10: GPIO_INTERRUPT_BOTH_PIN\n");
}

通过输入诸如-t\ -w \ -o等参数来配置GPIO的输入输出方式,检测输入输出的就不看了,可以看看下面的打开文件的操作:

devpath = argv[ndx];
printf("Driver: %s\n", devpath);

到了这一步他已经打开了GPIO的驱动文件。那么接下来就使用open来打开这个东西:

  fd = open(devpath, O_RDWR);
  if (fd < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: Failed to open %s: %d\n", devpath, errcode);
      return EXIT_FAILURE;
    }

之后是输入了这个GPIO的类型:

  /* Get the pin type */

  ret = ioctl(fd, GPIOC_PINTYPE, (unsigned long)((uintptr_t)&pintype));
  if (ret < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: Failed to read pintype from %s: %d\n", devpath,
              errcode);
      close(fd);
      return EXIT_FAILURE;
    }

使用的是函数ioctl,像配置寄存器一样将我们的GPIO的类型配置进寄存器中,配置的也是刚才在执行这个应用程序时所输入的参数。

之后就是读取GPIO的状态了:

  ret = ioctl(fd, GPIOC_READ, (unsigned long)((uintptr_t)&invalue));
  if (ret < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: Failed to read value from %s: %d\n",
              devpath, errcode);
      close(fd);
      return EXIT_FAILURE;
    }

之后根据这GPIO的类型去把输入输出高低电平来写入寄存器中,从而完成GPIO的配置,这段程序也就是所有GPIO用法的例程了:

  switch (pintype)
    {
      case GPIO_INPUT_PIN:
        {
          printf("  Input pin:     Value=%u\n",
                 (unsigned int)invalue);
        }
        break;

      case GPIO_INPUT_PIN_PULLUP:
        {
          printf("  Input pin (pull-up):     Value=%u\n",
                 (unsigned int)invalue);
        }
        break;

      case GPIO_INPUT_PIN_PULLDOWN:
        {
          printf("  Input pin (pull-down):     Value=%u\n",
                 (unsigned int)invalue);
        }
        break;

      case GPIO_OUTPUT_PIN:
      case GPIO_OUTPUT_PIN_OPENDRAIN:
        {
          printf("  Output pin:    Value=%u\n", (unsigned int)invalue);

          if (haveout)
            {
              printf("  Writing:       Value=%u\n", (unsigned int)outvalue);

              /* Write the pin value */

              ret = ioctl(fd, GPIOC_WRITE, (unsigned long)outvalue);
              if (ret < 0)
               {
                 int errcode = errno;
                 fprintf(stderr,
                         "ERROR: Failed to write value %u from %s: %d\n",
                         (unsigned int)outvalue, devpath, errcode);
                 close(fd);
                 return EXIT_FAILURE;
               }

              /* Re-read the pin value */

              ret = ioctl(fd, GPIOC_READ,
                          (unsigned long)((uintptr_t)&invalue));
              if (ret < 0)
                {
                  int errcode = errno;
                  fprintf(stderr,
                          "ERROR: Failed to re-read value from %s: %d\n",
                          devpath, errcode);
                  close(fd);
                  return EXIT_FAILURE;
                }

              printf("  Verify:        Value=%u\n", (unsigned int)invalue);
            }
        }
        break;

      case GPIO_INTERRUPT_PIN:
      case GPIO_INTERRUPT_HIGH_PIN:
      case GPIO_INTERRUPT_LOW_PIN:
      case GPIO_INTERRUPT_RISING_PIN:
      case GPIO_INTERRUPT_FALLING_PIN:
      case GPIO_INTERRUPT_BOTH_PIN:
        {
          printf("  Interrupt pin: Value=%u\n", invalue);

          if (havesigno)
            {
              struct sigevent notify;
              struct timespec ts;
              sigset_t set;

              notify.sigev_notify = SIGEV_SIGNAL;
              notify.sigev_signo  = signo;

              /* Set up to receive signal */

              ret = ioctl(fd, GPIOC_REGISTER, (unsigned long)&notify);
              if (ret < 0)
                {
                  int errcode = errno;

                  fprintf(stderr,
                          "ERROR: Failed to setup for signal from %s: %d\n",
                          devpath, errcode);

                  close(fd);
                  return EXIT_FAILURE;
                }

              /* Wait up to 5 seconds for the signal */

              sigemptyset(&set);
              sigaddset(&set, signo);

              ts.tv_sec  = 5;
              ts.tv_nsec = 0;

              ret = sigtimedwait(&set, NULL, &ts);
              ioctl(fd, GPIOC_UNREGISTER, 0);

              if (ret < 0)
                {
                  int errcode = errno;
                  if (errcode == EAGAIN)
                    {
                      printf("  [Five second timeout with no signal]\n");
                      close(fd);
                      return EXIT_SUCCESS;
                    }
                  else
                    {
                      fprintf(stderr, "ERROR: Failed to wait signal %d "
                              "from %s: %d\n", signo, devpath, errcode);
                      close(fd);
                      return EXIT_FAILURE;
                    }
                }

              /* Re-read the pin value */

              ret = ioctl(fd, GPIOC_READ,
                          (unsigned long)((uintptr_t)&invalue));
              if (ret < 0)
                {
                  int errcode = errno;
                  fprintf(stderr,
                          "ERROR: Failed to re-read value from %s: %d\n",
                          devpath, errcode);
                  close(fd);
                  return EXIT_FAILURE;
                }

              printf("  Verify:        Value=%u\n", (unsigned int)invalue);
            }
        }
        break;

      default:
        fprintf(stderr, "ERROR: Unrecognized pintype: %d\n", (int)pintype);
        close(fd);
        return EXIT_FAILURE;
    }

那么看到这里,写LED的闪烁应该有头绪了,只需要配置一下GPIO,输出高低电平即可,闪烁的话搞个while来实现就行了。

(二)驱动框架搭建

首先我们nuttx/drivers的目录下新建一个我们的工程文件,里面需要包含如下文件:

  • KConfig

  • Make.defs

  • LEDBling.c

  • LEDBling.h

 
 

通过这两张图就可以看出这个Kconfig文件的作用了,这个是图形化的关键,所以我们要先写好这个Kconfig。

menu "LEDBling"

config MY_LED_BLING
        bool "MY_LED_BLING device support"
        default n
        ---help---
                Enable driver support for the Led Bling controler.
endmenu #LEDBling
之后就可以在make menuconfig中看到我们新建的一个驱动的功能了,还可以选择它是否开启:

 

需要在本文件的上一个文件的Kcongfig中加入这句:

之后就可以在菜单的驱动列表中看到我们所写的。

接下来是编写Make.defs

# LEDBling

ifeq($(CONFIG_LEDBLING) ,y)
        CSRCS += LEDBling.c
#endif

那么我们先需要把底层的驱动先写好,首先我们应该有注册函数,并且要完善好上述所说的file_operations结构体以及对这个结构体进行注册,对于led这个驱动,其实本质就是操作GPIO,我们需要得知这个GPIO的状态,让它可读可写,所以需要封装可读写可注册设备的函数结构体,所以我们的LEDBling.h需要这么编写:

#ifndef _DRIVERS_LEDBLING_H
#define _DRIVERS_LEDBLING_H

#include <nuttx/config.h>
#include <nuttx/compiler.h>
#include <nuttx/fs/ioctl.h>


#define LEDBLING_SET  0
#define LEDBLING_GET  1

struct LEDBling_status
{
    bool state;         //存储IO口状态
};

struct LEDBling_param
{
    short num;
    struct LEDBling_status on;
};

struct LEDBling_regis_s
{
    CODE int (*setio)(FAR struct file * fp ,int cmd , unsigned long arg);
    CODE int (*getio)(FAR struct file * fp ,int cmd , unsigned long arg);
    CODE int (*ioctl)(FAR struct file * fp ,int cmd , unsigned long arg);
};

int LEDBling_register(FAR const char *path , FAR void *lower);

#endif

对于C文件,其实就是对这些函数的具体实现,以及读写IO口状态的具体实现,其实都是在操作io口的底层,我们还是在上一层。

#include <nuttx/config.h>
#include <stdlib.h>
#include <fixedmath.h>
#include <assert.h>
#include <errno.h>
#include <debug.h>
#include <nuttx/kmalloc.h>
#include <nuttx/fs/fs.h>
#include <nuttx/sensors/LEDBling.h>

#if defined (CONFIG_LEDBLING)
static int LEDBLING_open(FAR struct file * fp);
static int LEDBLING_close(FAR struct file * fp);
static int LEDBLING_ioctl(FAR struct file * fp ,int cmd , unsigned long arg);
static int LEDBLING_read(FAR struct file * fp , FAR char * buffer ,size_t buflen);
static int LEDBLING_write(FAR struct file * fp , FAR const char* buffer , size_t buflen);

static const struct file_operations LEDBLing_op = {
    .open = LEDBLING_open;
    .close = LEDBLING_close;
    .read = LEDBLING_read;
    .write = LEDBLING_write;
    .ioctl = LEDBLING_ioctl;
}

static int LEDBLING_open(FAR struct file * fp)
{
    return OK;
}

static int LEDBLING_close(FAR struct file * fp)
{
    return OK;
}

static int LEDBLING_read(FAR struct file * fp , FAR char * buffer ,size_t buflen)
{
    return OK;
}

static int LEDBLING_write(FAR struct file * fp , FAR const char* buffer , size_t buflen)
{
    return OK;
}

static int LEDBLING_ioctl(FAR struct file * fp ,int cmd , unsigned long arg)
{
    FAR struct inode * inode  = fp -> f_inode;
    FAR struct LEDBling_regis_s *lower = inode->i_private;
    int ret = OK;

    switch(cmd)
    {
        // 这个函数是设置GPIO的状态函数
        case LEDBLING_SET:
            FAR struct LEDBling_param param = {0};
            param.num = arg & 0xFF;
            param.on.state = (arg >> 8) & 0x03;
            ret = lower -> setio(lower,param);
        break;

        case LEDBLING_GET:
            FAR struct LEDBling_status *ptr = (FAR struct LEDBling_status*)((uintptr_t)arg);
            DEBUGASSERT(ptr!=NULL);
            ret = lower-> getio(lower,ptr);
        break;

        default:
            ret  = lower -> ioctl(lower,cmd,arg);
        break;
             
    }
    return ret;
}

int LEDBling_register(FAR const char *path,FAR struct LEDBling_regis_s *dev,FAR void* lower)
{
    int ret;

    DEBUGASSERT(path != NULL);
    DEBUGASSERT(lower != NULL);

    ret = register_driver(path, &LEDBLing_op, 0666, lower);

    if (ret < 0){
        _err("ERROR: Failed to register slim led driver: %d\n", ret);
    }

    return ret;

}

写完下层驱动后,我们需要看看上层调用实现,要与具体芯片平台或者是board平台进行绑定。构建设备驱动程序要与硬件的桥梁,接下来需要去board仓库中修改如下文件:

  • Make.defs

  • config/nsh/deconfig

  • board_def.h

  • bringup.c

  • platform_data.c

  • board_LEDBling.h

  • board_LEDBling.c

那么首先我们需要在Make.defs加上下面这句:

ifeq ($(CONFIG_LEDBLING),y)
	CSRCS += board_LEDBling.c;
endif

 之后修改defconfig,路径于:/nuttxspace/nuttx/boards/arm/stm32/stm32f429i-disco/configs/nsh:

CONFIG_LEDBLING=y

更改stm32f429i-disco.h

#define BLING_LED_R       (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_50MHz|\
						 GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN10)
#define BLING_LED_G       (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_50MHz|\
                         GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN11)
#define BLING_LED_B       (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_50MHz|\
                         GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN12)

对应的开发板引脚号:

更改源代码的bringup.c

int stm32_bringup(void)
{
    ...
    #ifdef CONFIG_LEDBLING
    ret =  board_ledbling_initialize("/dev/ledbling",&ledbling_data,eLED_CNT);
    if(ret<0)
    {
        printf("ERROR:LedBling driver initialize failed:%d\n",ret);
    }
    #endif
    ...
}

新建board_LEDBling.h/nuttxspace/nuttx/boards/arm/stm32/stm32f429i-disco/include:

#ifndef _BOARD_LEDBLING_H
#define _BOARD_LEDBLING_H

#include <nuttx/config.h>
#include <nuttx/regulator/fixed-regulator.h>

typedef enum{
    eLED_R,
    eLED_G,
    eLED_B,
    eLED_CNT
}LEDBling_Num;

struct ledbling_platform_data{
    uint32_t pin;
    bool	 state;
};

const struct ledbling_platform_data ledbling_data[]={
    {
        .pin = BLING_LED_R,
        .state = false,
    },
    {
        .pin = BLING_LED_G,
        .state = false,
    },
    {
        .pin = BLING_LED_B,
        .state = false,
    },
} ;

extern const struct ledbling_platform_data ledbling_data[];
int board_ledbling_initialize(const char *devpath,const struct ledbling_platform_data *data,uint32_t cnt);

#endif
新建board_LEDBling.c于/nuttx/boards/arm/stm32/stm32f429i-disco/src: 
#include <nuttx/nuttx.h>
#include <nuttx/irq.h>
#include <nuttx/signal.h>
#include <debug.h>
#include <assert.h>
#include <errno.h>

#include <nuttx/err.h>
#include <nuttx/boards/arm/stm32/stm32f429i-disco/srcstm32f429i-disco.h>
#include <nuttx/boards/arm/stm32/stm32f429i-disco/include/board_LEDBling.h>
#include <nuttx/sensors/LEDBling.h>

#ifdef CONFIG_LEDBLING
static bool _get_LEDBling_state(FAR struct file * fp ,int cmd , unsigned long arg);
static void _set_LEDBling_param(FAR struct file * fp ,int cmd , unsigned long arg);

static const struct LEDBling_regis_s LEDBling_regis = {
    .getio = _get_LEDBling_state,
    .setio = _set_LEDBling_param,
    .ioctl = _set_LEDBling_param,
};

static bool _get_LEDBling_state(FAR struct file * fp ,int cmd , unsigned long arg)
{
    return LEDBLING_ioctl(fp,cmd,arg);
}

static void _set_LEDBling_param(FAR struct file * fp ,int cmd , unsigned long arg)
{
    printf("%s %d %d\r\n",__func__,param.num,param.on.state);
    if(param.num < eLED_CNT)
    {
        LEDBLING_ioctl(fp,cmd,arg);
    }
}    

int board_ledbling_initialize(const char *devpath,const struct ledbling_platform_data *data,uint32_t cnt)
{
    if(cnt>eLED_CNT){
        printf("ERROR: LED set error...\n");
        cnt = eLED_CNT;
    }
    for(uint8_t i=0;i<cnt;i++)
    {
        configgpio(pdata[i].pin);
    }
    
    return LEDBling_register(devpath,&LEDBling_regis);
}

那么到此为止,我们实现了对自己设备的驱动封装,通过函数指针去实现,完成了read\write\ioctl,完成了设备的注册,那么到此为止,我们可以生成一个/dev/ledbling的设备。那么看看应用层:

#include <stdio.h>
#include <stdbool.h>
#include <sys/types.h>
#include <fcntl.h>
#include <pthread.h>
#include <nuttx/config.h>
#include <nuttx/analog/ioctl.h>
#include <LEDBling.h>
#include <syslog.h>

typedef enum{
    eLED_R,
    eLED_G,
    eLED_B,
    eLED_CNT
}LEDBling_Pin_all;

static int led_fd = -1;

static bool _LEDBling_init(void)
{
    led_fd  = open("/dev/ledbling",O_WRONLY);
    if(led_fd<0)
    {
        printf("LEDBling open failed\n");
        return false;
    }
    return true;
}

static bool _set_ledbling_channel(LEDBling_Pin_all channel)
{
    struct ledbling_platform_data tmp[eLED_CNT] = {0};
    if(channel > eLED_CNT)
    {
        printf("param setting error...\n");
        return false;
    }
    
    if(led_fd<0)
    {
        printf("ledbling not opened yet\n");
        return _LEDBling_init();
    }
    for(uint8_t i=0;i<eLED_CNT;i++)
    {
        tmp[i] = i;
        if(i==channel)
        {
            tmp[i].on.state = true;
        }else{
            tmp[i].on.state = false;
        }
        _set_LEDBling_param(led_fd,LEDBLING_SET,tmp[i]);
    }
}

//线程操作
void *ledbling_thread(void *arg)
{
  	int ret = 0;
    int sta = 0;
    
    if(false==_LEDBling_init())	return;
    for(;;)
    {
        for(uint8_t i=0;i<eLED_CNT;i++)
        {
            _set_ledbling_channel(i);
            sleep(5);
        }
        _get_LEDBling_state(led_fd , LEDBLING_GET , &sta);
        printf("ioctl state : %d get IO state : %d",ret,sta);
    }
    close(led_fd);
    return;
}

0x05 使用已经封装好的驱动来点灯

(一)修改库文件

可以看一下目录/nuttxspace/nuttx/drivers/leds下的源文件userled_upper.c以及userled_lower.c,那么在目录下/nuttxspace/nuttx/include/nuttx/leds可以找到其userled.h,其文件中有函数:

userled_lower.c:也就是我们与嵌入式板的硬件相连接的底层文件,可以看看它的结构:

#include <nuttx/config.h>

#include <sys/types.h>
#include <assert.h>
#include <debug.h>
#include <inttypes.h>

#include <nuttx/board.h>
#include <nuttx/leds/userled.h>

包含如上头文件,那么在userled.h文件中声明的可以看看,这里封装了一些对GPIO的操作,使用宏来进行封装以及定义,接下来使用一个结构体保存led的引脚以及状态,最后声明了一个函数结构体去封装LED的操作:

struct userled_lowerhalf_s
{
  /* Return the set of LEDs supported by the board */

  CODE userled_set_t
  (*ll_supported)(FAR const struct userled_lowerhalf_s *lower);

  /* Set the current state of one LED */

  CODE void (*ll_setled)(FAR const struct userled_lowerhalf_s *lower,
                         int led, bool ledon);

  /* Set the state of all LEDs */

  CODE void (*ll_setall)(FAR const struct userled_lowerhalf_s *lower,
                         userled_set_t ledset);

#ifdef CONFIG_USERLED_LOWER_READSTATE
  /* Get the state of all LEDs */

  CODE void (*ll_getall)(FAR const struct userled_lowerhalf_s *lower,
                         userled_set_t *ledset);
#endif
};

最后也需要写一个注册函数:

int userled_register(FAR const char *devname,
                     FAR const struct userled_lowerhalf_s *lower);

那么对于userled_lower.c就是要实例化这个头文件的设置,可以看到它在这里把结构体userled_lowerhalf_s进行了实例化:

static userled_set_t
userled_supported(FAR const struct userled_lowerhalf_s *lower);
static void userled_setled(FAR const struct userled_lowerhalf_s *lower,
                           int led, bool ledon);
static void userled_setall(FAR const struct userled_lowerhalf_s *lower,
                           userled_set_t ledset);
#ifdef CONFIG_USERLED_LOWER_READSTATE
static void userled_getall(FAR const struct userled_lowerhalf_s *lower,
                           userled_set_t *ledset);
#endif

static uint32_t g_lednum;

/* This is the user LED lower half driver interface */

static const struct userled_lowerhalf_s g_userled_lower =
{
  userled_supported,    /* ll_supported */
  userled_setled,       /* ll_setled */
  userled_setall        /* ll_setall */
#ifdef CONFIG_USERLED_LOWER_READSTATE
  , userled_getall      /* ll_getall */
#endif
};

并且声明了这些函数后我们需要对其进行对gpio口操作的实现,那么这个时候就需要完成如下:

static void userled_setled(FAR const struct userled_lowerhalf_s *lower,
                           int led, bool ledon)
{
  board_userled(led, ledon);
}

static void userled_setall(FAR const struct userled_lowerhalf_s *lower,
                           userled_set_t ledset)
{
  board_userled_all(ledset);
}

static void userled_getall(FAR const struct userled_lowerhalf_s *lower,
                           userled_set_t *ledset)
{
  board_userled_getall(ledset);
}

int userled_lower_initialize(FAR const char *devname)
{
  g_lednum = board_userled_initialize();
  return userled_register(devname, &g_userled_lower);
}

有点套娃的感觉了,去看看board_userled这些函数吧,在路径/nuttxspace/nuttx/include/nuttx下可以看到这个board.h文件,这里也就只是这些函数的声明,可以在/nuttxspace/nuttx/boards/arm/stm32/stm32f429i-disco/src下找到stm32_userled.c,在其中可以看到我们的引脚的定义以及上面那些函数的实例:

 如果要使我们自己板上的LED进行闪烁变换的话,我们需要改这几个引脚的IO口就可以了,在这里我们需要去文件stm32f429i-disco.h中添加:

之后对其进行替换即可。以上就是底层的实例的文件,这个时候我们需要打开例程看看其如何操作的,具体的操作是在/nuttxspace/apps/examples/leds。打开后我们就可以看看其应用层的使用了:

对于主函数:使用task_create函数开启一个任务,并且输入我们的操作设备的函数、分配堆栈的大小:

ret = task_create("led_daemon", CONFIG_EXAMPLES_LEDS_PRIORITY,
                    CONFIG_EXAMPLES_LEDS_STACKSIZE, led_daemon,
                    NULL);

那么重点看看这个led_daemon的函数:

 

 在这里就使用了在Kconfig中提到的,由图形化界面来选择是否开启的一些宏参数:

我们现在只有一个灯,就需要吧这个EXAMPLES_LEDS_LEDSET改为0x01。之后在菜单中把这个例程开启就可以试一试了。

解决型号不匹配的问题,由于在选择型号时并没有选择自己开发板的型号,所以当烧录完系统后可能会出现晶振频率不匹配且各种频率匹配不上的问题,所以我们需要到board.h文件下修改这个晶振频率以及各种分频系数,路径为/nuttx/boards/arm/stm32/stm32f429i-disco/include,修改后的结果如下(这里使用的野火STM32F429IGT6的挑战者V1开发板):

之后修改为如下:

 

 解决打印出来会乱码的问题,首先是觉得波特率配错了,但是进入了图形化界面时,看到了波特率确实115200,并没有错,但是与minicom中的停止位不符合,于是要做出如下修改:

那么它就会把这个系统的打印输出到终端上。

百度一下这个错:up_assert: Assertion failed at file:armv7-m/arm_hardfault.c line: 175

那么接下来就开始编写程序吧,这里我们使用注册好的驱动/dev/userleds,首先我们需要去菜单下打开LED驱动:

 之后于Application Configuration中打开LED的例程:

 之后我们对LED的例程进行修改,在板子上有一个RGB的灯,并且我们可以得到灯的引脚号,那么我们进入stm32_userled.c中进行修改,然后在stm32f429i-disco.h中加入对应的IO口,并且更改.c文件中的各种操作指定的IO口的定义即可。

猜你喜欢

转载自blog.csdn.net/Alkaid2000/article/details/127955103
今日推荐