基于RT-Thread-nano的STM32 PIN设备移植

RT-Thread-nano版本是一个极简内核版本,非常适合应用于资源紧张的MCU场景,但是在MCU中最常用的PIN设备却没有包含进来,好在PIN设备的移植也不复杂。

1、移植之前我们需要先下载好RT-Thread标准版本,因为我们需要用到标准版本里面的pin.c和pin.h两个文件。

2、当然移植之前我们还需要有一个没有问题的移植好RT-Thread-nano内核的工程。

3、在rt-thread\components目录下创建一个drivers文件夹并将标准版本里面的rt-thread-master\components\drivers\misc路径下的pin.c文件拷贝到新建的drivers文件夹中。

4、将标准版本里面的rt-thread-master\components\drivers\include\drivers路径下的pin.h文件拷贝到新建的drivers文件夹中。

5、将.c文件和.h文件包含到工程中,其中pin.c文件在工程中单独新建一个rtthread/drivers组放置。

6、打开pin.c文件,并将头文件#include <drivers/pin.h>改成#include <pin.h>。

7、往下拉可以看到第80行报了个错,报这个错的原因是RT-Thread-nano版本中没有定义RT_Device_Class_Pin这个设备类型,在RT-Thread-nano版本中PIN设备类型为RT_Device_Class_Miscellaneous,是在rtdef.h文件中定义的。用RT_Device_Class_Miscellaneous替换掉RT_Device_Class_Pin后报错消除。

8、从170行的#ifdef RT_USING_FINSH开始到这个文件的结束部分我们暂时还用不到,先添加#if 0 注释掉。

9、编译验证下这两个文件还有没有错,如果上面的操作正确的话编译是没有报错的。

10、从135行到168行就是pin设备封装好后的统一的操作函数,应用层只需调用者几个函数就能实现了对底层IO的操作,同时屏蔽了不同硬件的差异。

11、到这里,你以为移植就已经完成啦?这才刚刚开始呢,最重要的是要实现底层的操作,这部分是跟硬件相关的,所以是需要我们自己来实现的。

12、从pin文件给我们提供的几个对外接口可以看到,应用层最终调用的实际上是_hw_pin.ops里面的方法来实现各种操作的,这也就意味着我们是要具体实现这些方法的。

13、虽然知道了我们要实现底层的操作方法,但是我们实现的底层操作方法是怎么跟这个 _hw_pin.ops 关联起来的呢?实际上是调用注册函数 rt_device_pin_register 将底层操作方法注册到了设备里面。这个函数在要初始化的时候调用以便完成PIN设备操作方法的注册,在被调用之前PIN设备是无法正常使用的,一般是在rt_hw_board_init()中调用,可以是显示调用也可以是隐式调用。

14、要调用 rt_device_pin_register 这个函数需要准备好一些入口参数, name 就是设备名称, user_data 是用户数据,这两个都好解决,名称随便取一个有意义的字符串就行,数据没有的话可以为空( RT_NULL )。重点是要实现 ops , ops 是一个结构体,在pin.h中定义,如下图所示。所以最终我们的目标就变成了要实现这几个底层操作函数,其中前三个是非常常用的,在使用PIN设备时基本都会用到的,所以我们先实现前面三个。

struct rt_pin_ops
{
    void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_base_t mode);
    void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_base_t value);
    int (*pin_read)(struct rt_device *device, rt_base_t pin);
    rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_int32_t pin,
                      rt_uint32_t mode, void (*hdr)(void *args), void *args);
    rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_int32_t pin);
    rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint32_t enabled);
    rt_base_t (*pin_get)(const char *name);
};

15、我们先尝试着自己去简单实现,后面再讲下怎么去移植其他demo中的实现方式,这几个函数的实现我们统一放到STM32CubeMX创建的gpro.c文件中。

16、首先是pin_mode函数的实现,GPIO的初始化都是在这个函数实现的,包括时钟的开启和IO口的配置,实现大体步骤如下:
1)由pin(待操作的IO口在IO列表中的位置)找到其对应的GPIO POART和PIN,该函数需要自己实现
2)填充初始化参数
3)使能该POART的时钟
4)调用初始化函数初始化IO口
具体的实现代码如下:

static void stm32_pin_mode(struct rt_device *device, rt_base_t pin, rt_base_t mode)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  struct _pin_inf pin_inf = {0};

  // 1.由pin(待操作的IO口在IO列表中的位置)找到其对应的GPIO POART和PIN
  pin_inf = get_stm32_pin_inf(pin);
  // end 1

  //  2.填充初始化参数
  GPIO_InitStruct.Pin = pin_inf.gpio_pin;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;

  switch (mode)
  {
  case PIN_MODE_OUTPUT:
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
    break;

  case PIN_MODE_OUTPUT_OD:
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
    break;

  case PIN_MODE_OUTPUT_OD_PULLDOWN:
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
      GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    break;

  case PIN_MODE_OUTPUT_OD_PULLUP:
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
    break;

  case PIN_MODE_OUTPUT_PULLUP:
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
    break;

  case PIN_MODE_OUTPUT_PULLDOWN:
      GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
      GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    break;

  case PIN_MODE_INPUT:
      GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
    break;

  case PIN_MODE_INPUT_PULLUP:
      GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
    break;

  case PIN_MODE_INPUT_PULLDOWN:
      GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
      GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    break;

  default:
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    break;
  }
  // end 2

  // 3.使能该POART的时钟
  switch ((uint32_t)pin_inf.gpio_port)
  {
#ifdef GPIOA
  case (uint32_t)GPIOA:
    __HAL_RCC_GPIOA_CLK_ENABLE();
    break;
#endif

#ifdef GPIOB  
  case (uint32_t)GPIOB:
    __HAL_RCC_GPIOB_CLK_ENABLE();
    break;
#endif

#ifdef GPIOC
  case (uint32_t)GPIOC:
    __HAL_RCC_GPIOC_CLK_ENABLE();
    break;
#endif

#ifdef GPIOD  
  case (uint32_t)GPIOD:
    __HAL_RCC_GPIOD_CLK_ENABLE();
    break;
#endif

#ifdef GPIOE
  case (uint32_t)GPIOE:
    __HAL_RCC_GPIOE_CLK_ENABLE();
    break;
#endif

#ifdef GPIOF
  case (uint32_t)GPIOF:
    __HAL_RCC_GPIOF_CLK_ENABLE();
    break;
#endif

#ifdef GPIOG
  case (uint32_t)GPIOG:
    __HAL_RCC_GPIOG_CLK_ENABLE();
    break;
#endif

#ifdef GPIOH  
  case (uint32_t)GPIOH:
    __HAL_RCC_GPIOH_CLK_ENABLE();
    break;
#endif

  default:
    break;
  }
  // end 3

  // 4.调用初始化函数初始化IO口
  HAL_GPIO_Init(pin_inf.gpio_port, &GPIO_InitStruct);
  // end 4

}

其中 get_stm32_pin_inf 函数实现如下:

static struct _pin_inf get_stm32_pin_inf(rt_base_t pin)
{
  struct _pin_inf pin_inf;
  rt_uint32_t port_num;
  port_num = pin / 16;
  // 一个GPIO PORT的空间大小为0x00000400
  pin_inf.gpio_port = (GPIO_TypeDef*)((rt_uint32_t)GPIOA + (rt_uint32_t)port_num *  0x00000400);

  pin_inf.gpio_pin = (uint16_t)(1u << (pin % 16));
  return pin_inf;
}

_pin_inf 结构体定义如下:

struct _pin_inf
{
  GPIO_TypeDef *gpio_port;
  rt_uint16_t gpio_pin;
};

17、接下来实现 stm32_pin_write 和 stm32_pin_read,这两个函数实现起来就容易和简单很多:

static void stm32_pin_write(struct rt_device *device, rt_base_t pin, rt_base_t value)
{
  struct _pin_inf pin_inf = {0};
  pin_inf = get_stm32_pin_inf(pin);

  HAL_GPIO_WritePin(pin_inf.gpio_port, pin_inf.gpio_pin, (GPIO_PinState)value);
}

static int stm32_pin_read(struct rt_device *device, rt_base_t pin)
{
  struct _pin_inf pin_inf = {0};
  pin_inf = get_stm32_pin_inf(pin);

  return HAL_GPIO_ReadPin(pin_inf.gpio_port, pin_inf.gpio_pin);
}

18、三个最重要也是最常用的操作方法实现了,接下来先不管剩下的其他几个函数,先将其按 rt_pin_ops 的格式将这三个操作方法封装成结构体:

const static struct rt_pin_ops _stm32_pin_ops =
{
    stm32_pin_mode,
    stm32_pin_write,
    stm32_pin_read,
    RT_NULL,
    RT_NULL,
    RT_NULL,
    RT_NULL,
};

19、封装好后接下来就是调用注册函数 rt_device_pin_register 将操作方法绑定到设备中并将设备注册进设备列表中,由于 rt_device_pin_register 需要传参进来,为了参数不在该文件外传递,再将其封装一层,并将其导出到板级初始化列表中,这样在系统起来后便会自动完成注册了。

int rt_hw_pin_register(void)
{
  return rt_device_pin_register("pin", &_stm32_pin_ops, RT_NULL);
}
INIT_BOARD_EXPORT(rt_hw_pin_register);  //将注册函数导出到板级初始化列表,隐式调用完成注册

20、最基本的驱动层到这里就结束啦,接下来就是应用层上怎么运用了,应用层上我们只需要掉用pin.c文件提供的几个接口函数,这个几个接口函数就会找到我们上面定义好并绑定到设备的对应的操作方法来完成我们的需求。供应用层调用的几个接口函数如下所示,我们在此暂时只关注最基础的前面三个:

void rt_pin_mode(rt_base_t pin, rt_base_t mode);
void rt_pin_write(rt_base_t pin, rt_base_t value);
int  rt_pin_read(rt_base_t pin);
rt_err_t rt_pin_attach_irq(rt_int32_t pin, rt_uint32_t mode,
                             void (*hdr)(void *args), void  *args);
rt_err_t rt_pin_detach_irq(rt_int32_t pin);
rt_err_t rt_pin_irq_enable(rt_base_t pin, rt_uint32_t enabled);
/* Get pin number by name,such as PA.0,P0.12 */
rt_base_t rt_pin_get(const char *name);

21、可以看到,这几个函数里面都有一个 pin 的参数是要传进去的,这个pin指的是待操作的IO口在整个芯片的IO口中的位置,比如GPIOA0对应的pin为0,GPIOA15对应的pin为15,GPIOB0对应的pin为16,GPIOB1对应的pin位17,如此类推。这其实对于使用来说非常不友好,比如假设我要控制的IO口是GPIOE13,那pin这个参数应该填啥?是不是还得花费时间去算下?为此我们需要再做点处理。我们定义一个输入简单可容易辨识的参数便能自动完成pin计算的宏GET_PIN(PORTx,PIN),使用的时候由宏自动帮我们算出pin即可。我们将这两个宏放放到gpio.h文件中,其实现方法如下:

#define __STM32_PORT(port)  GPIO##port##_BASE
#define GET_PIN(PORTx,PIN) (rt_base_t)((16 * ( ((rt_base_t)__STM32_PORT(PORTx) - (rt_base_t)GPIOA_BASE)/(0x0400UL) )) + PIN)

其中#define __STM32_PORT(port) GPIO##port##_BASE 中的双井号为字符连接符,假设传入的参数为 A ,则该该宏展开后为:GPIOA_BASE

22、有了上面的宏,我们在应用层上就友好多了,在代用函数前先用宏定义一个方便我们识别的名称,比如下图所示,这样LD4表示的就是上述步骤20中的接口函数对应的pin,从用户的可读性上来说用户同时也能很清楚的直到LD4这颗LED是有PE10来控制的。

#define LD4     GET_PIN(E, 10)
#define LD3     GET_PIN(E, 11)

23、接下来就是实现对IO口的控制了,在控制之前还要进行IO口的配置和时钟的使能,这两部分工作都是由 rt_pin_mode 来完成的,所以在控制IO口之前必须要先调用 rt_pin_mode 。

#define LD4     GET_PIN(E, 10)
#define LD3     GET_PIN(E, 11)

void app1(void *p)
{
    rt_pin_mode(LD4, PIN_MODE_OUTPUT);
    rt_pin_mode(LD3, PIN_MODE_OUTPUT);
    while(1)
    {
        rt_kprintf("APP1 Running!\n");
        rt_pin_write(LD4, 1);
        rt_pin_write(LD3, 1);
        rt_thread_mdelay(200);
        rt_pin_write(LD4, 0);
        rt_pin_write(LD3, 0);
        rt_thread_mdelay(200);
    }
   
}

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值