RT-Thread I/O设备模型及驱动框架解析(一)

目录

 

1. 概述

2. 原理解析

3. 源码解析

3.1. 创建设备

3.2.  注册到驱动框架

3.3. 注册到IO设备管理器

 4. 小结

1. 概述

本着由简入繁的原则,分析源码以STM32平台的看门狗源码为例,正好参考官方资料辅助学习下。

硬件平台及软件版本如下:

硬件平台:STM32F407ZG

RT-Thread版本:4.0.4

在分析源码前需要了解的基础知识如下:

自动初始化机制

RT-Thread 文档中心

I/O驱动模型

RT-Thread 文档中心

看门狗驱动框架

RT-Thread 文档中心

2. 原理解析

通过基础知识的准备,切回到我们的主题。那么在I/O设备模型下,使用watchdog驱动框架如何驱动硬件看门狗呢?

看下官方资料给出的流程图:

通过这张框图明确的流程是

1,创建看门狗设备,并实现底层驱动

2,注册看门狗设备到看门狗设备驱动框架

3,注册I/O设备到I/O设备管理器

4,应用程序使用看门狗

3. 源码解析

原则上说,分析源码是要明确框架才能进一步分析,但是为了方便与简化理解,不妨从设备驱动开始,往上层一步一步追踪来研究源码。

几个关键的RT-Thread的代码目录

设备的驱动代码在

libraries/HAL_Drivers

驱动框架

rt-thread/components/drivers

设备模型

rt-thread/src

3.1. 创建设备

按照上面的流程图理解,首先是要创建看门狗设备,这时看驱动文件drv_wdt.c。

看门狗设备的结构体定义如下,该看门狗设备采用静态初始化的方法,定义了看门狗设备对象及看门狗设备的操作方法。

struct stm32_wdt_obj
{
    //看门狗设备定义
    rt_watchdog_t watchdog;
    //看门狗的硬件结构体定义
    IWDG_HandleTypeDef hiwdg;
    //是否初始化的标志
    rt_uint16_t is_start;
};
//看门狗实例
static struct stm32_wdt_obj stm32_wdt;
//看门狗的操作方法
static struct rt_watchdog_ops ops;

其中使用了看门狗驱动框架的看门狗相关的结构体定义,后面在说。硬件结构体,就是stm32官方的定义,这个可以去看官方驱动示例。那么结合结构体的定义,看门狗的初始化如下,主要是配置硬件参数,然后向驱动框架中注册该设备,名字即为“wdt”。

int rt_wdt_init(void)
{
    //看门狗硬件参数配置
#if defined(SOC_SERIES_STM32H7)
    stm32_wdt.hiwdg.Instance = IWDG1;
#else
    stm32_wdt.hiwdg.Instance = IWDG;
#endif
    stm32_wdt.hiwdg.Init.Prescaler = IWDG_PRESCALER_256;

    stm32_wdt.hiwdg.Init.Reload = 0x00000FFF;
#if defined(SOC_SERIES_STM32F0) || defined(SOC_SERIES_STM32L4) || defined(SOC_SERIES_STM32F7) \
    || defined(SOC_SERIES_STM32H7) || defined(SOC_SERIES_STM32L0)
    stm32_wdt.hiwdg.Init.Window = 0x00000FFF;
#endif
    stm32_wdt.is_start = 0;

    //操作方法的绑定
    ops.init = &wdt_init;
    ops.control = &wdt_control;
    stm32_wdt.watchdog.ops = &ops;

    //向驱动框架中注册该设备
    if (rt_hw_watchdog_register(&stm32_wdt.watchdog, "wdt", RT_DEVICE_FLAG_DEACTIVATE, RT_NULL) != RT_EOK)
    {
        LOG_E("wdt device register failed.");
        return -RT_ERROR;
    }
    LOG_D("wdt device register success.");
    return RT_EOK;
}
INIT_BOARD_EXPORT(rt_wdt_init);

 此处看门狗设备的创建采用自动初始化机制,按照自动初始化机制描述,该初始化是板级初始化。初始化相关宏如下:


接下来再看下,看门狗设备定义的驱动功能,就两个,一个是看门狗的初始化,另一个是看门狗的各种功能控制(喂狗、设置超时时间等)。此处的初始化是一个空函数,看门狗的初始化是通过

static rt_err_t wdt_control(rt_watchdog_t *wdt, int cmd, void *arg)

结合RT_DEVICE_CTRL_WDT_START命令来实现的。

static rt_err_t wdt_init(rt_watchdog_t *wdt)
{
    return RT_EOK;
}

static rt_err_t wdt_control(rt_watchdog_t *wdt, int cmd, void *arg)
{
    switch (cmd)
    {
        /* feed the watchdog */
    case RT_DEVICE_CTRL_WDT_KEEPALIVE:
        if(HAL_IWDG_Refresh(&stm32_wdt.hiwdg) != HAL_OK)
        {
            LOG_E("watch dog keepalive fail.");
        }
        break;
        /* set watchdog timeout */
    case RT_DEVICE_CTRL_WDT_SET_TIMEOUT:
#if defined(LSI_VALUE)
        if(LSI_VALUE)
        {
            stm32_wdt.hiwdg.Init.Reload = (*((rt_uint32_t*)arg)) * LSI_VALUE / 256 ;
        }
        else
        {
            LOG_E("Please define the value of LSI_VALUE!");
        }
        if(stm32_wdt.hiwdg.Init.Reload > 0xFFF)
        {
            LOG_E("wdg set timeout parameter too large, please less than %ds",0xFFF * 256 / LSI_VALUE);
            return -RT_EINVAL;
        }
#else
  #error "Please define the value of LSI_VALUE!"
#endif
        if(stm32_wdt.is_start)
        {
            if (HAL_IWDG_Init(&stm32_wdt.hiwdg) != HAL_OK)
            {
                LOG_E("wdg set timeout failed.");
                return -RT_ERROR;
            }
        }
        break;
    case RT_DEVICE_CTRL_WDT_GET_TIMEOUT:
#if defined(LSI_VALUE)
        if(LSI_VALUE)
        {
            (*((rt_uint32_t*)arg)) = stm32_wdt.hiwdg.Init.Reload * 256 / LSI_VALUE;
        }
        else
        {
            LOG_E("Please define the value of LSI_VALUE!");
        }
#else
  #error "Please define the value of LSI_VALUE!"
#endif
        break;
    case RT_DEVICE_CTRL_WDT_START:
        if (HAL_IWDG_Init(&stm32_wdt.hiwdg) != HAL_OK)
        {
            LOG_E("wdt start failed.");
            return -RT_ERROR;
        }
        stm32_wdt.is_start = 1;
        break;
    default:
        LOG_W("This command is not supported.");
        return -RT_ERROR;
    }
    return RT_EOK;
}

那么通过以上的源码就实现了硬件层面的看门狗的创建,或者说参数配置及功能实现。

接下来就是注册看门狗设备到看门狗设备框架了。

3.2.  注册到驱动框架

上一小节的的看门狗初始化中调用了注册函数,该函数即实现了注册到驱动框架的功能。

if (rt_hw_watchdog_register(&stm32_wdt.watchdog, "wdt", RT_DEVICE_FLAG_DEACTIVATE, RT_NULL) != RT_EOK)
{
    LOG_E("wdt device register failed.");
    return -RT_ERROR;
}

看门狗的驱动框架实现见watchdog.c,注册函数的实现如下:

rt_err_t rt_hw_watchdog_register(struct rt_watchdog_device *wtd,
                                 const char                *name,
                                 rt_uint32_t                flag,
                                 void                      *data)
{
    struct rt_device *device;
    RT_ASSERT(wtd != RT_NULL);

    device = &(wtd->parent);

    device->type        = RT_Device_Class_Security;
    //回调处理
    device->rx_indicate = RT_NULL;
    device->tx_complete = RT_NULL;

//看门狗设备的操作方法
#ifdef RT_USING_DEVICE_OPS
    device->ops         = &wdt_ops;
#else
    device->init        = rt_watchdog_init;
    device->open        = rt_watchdog_open;
    device->close       = rt_watchdog_close;
    device->read        = RT_NULL;
    device->write       = RT_NULL;
    device->control     = rt_watchdog_control;
#endif
    device->user_data   = data;

    /* register a character device */
    return rt_device_register(device, name, flag);
}

此处,就是把看门狗设备的类型、回调函数、操作方法设置或绑定好,由于看门狗设备操作简单,此处的回调直接为空。

其中RT_USING_DEVICE_OPS宏,个人理解是为了兼容不同版本的问题,除了ram和rom使用大小有区别,其他没有什么影响。

接下来看下看门狗框架中的设备操作方法的实现,按照官方的I/O设备模型需要实现如下操作方法。

 在看门狗设备中,由于不需要使用读写功能,则只定义了初始化、打开、关闭、控制的函数。如下的操作方法实现,最终都是调用上一小结中驱动功能的初始化功能控制来实现的。

static rt_err_t rt_watchdog_init(struct rt_device *dev)
{
    rt_watchdog_t *wtd;

    RT_ASSERT(dev != RT_NULL);
    wtd = (rt_watchdog_t *)dev;
    if (wtd->ops->init)
    {
        return (wtd->ops->init(wtd));
    }

    return (-RT_ENOSYS);
}

static rt_err_t rt_watchdog_open(struct rt_device *dev, rt_uint16_t oflag)
{
    return (RT_EOK);
}

static rt_err_t rt_watchdog_close(struct rt_device *dev)
{
    rt_watchdog_t *wtd;

    RT_ASSERT(dev != RT_NULL);
    wtd = (rt_watchdog_t *)dev;

    if (wtd->ops->control(wtd, RT_DEVICE_CTRL_WDT_STOP, RT_NULL) != RT_EOK)
    {
        rt_kprintf(" This watchdog can not be stoped\n");

        return (-RT_ERROR);
    }

    return (RT_EOK);
}

static rt_err_t rt_watchdog_control(struct rt_device *dev,
                                    int              cmd,
                                    void             *args)
{
    rt_watchdog_t *wtd;

    RT_ASSERT(dev != RT_NULL);
    wtd = (rt_watchdog_t *)dev;

    return (wtd->ops->control(wtd, cmd, args));
}

通过以上源码,就实现了看门狗设备注册到驱动框架中,接下来就是把它注册到IO设备管理器中。

3.3. 注册到IO设备管理器

注册到IO设备管理器的函数如下,在文件device.c中。

rt_device_register(device, name, flag);

 函数实现如下:

rt_err_t rt_device_register(rt_device_t dev,
                            const char *name,
                            rt_uint16_t flags)
{
    if (dev == RT_NULL)
        return -RT_ERROR;

    if (rt_device_find(name) != RT_NULL)
        return -RT_ERROR;

    rt_object_init(&(dev->parent), RT_Object_Class_Device, name);
    dev->flag = flags;
    dev->ref_count = 0;
    dev->open_flag = 0;

#ifdef RT_USING_POSIX
    dev->fops = RT_NULL;
    rt_wqueue_init(&(dev->wait_queue));
#endif /* RT_USING_POSIX */

    return RT_EOK;
}
RTM_EXPORT(rt_device_register);

到此为止,看门狗设备就被注册到IO设备管理器中了,可以使用IO设备管理接口操作看门狗设备了。

 4. 小结

总体来看,要明白这套驱动框架及模式,首先要理解整体流程,其次是RT-Thread的设备对象,所有的设备都是通过设备基类派生出来的,或者用结构体的方式理解就是具体的设备类结构体包含了设备基类的结构体。

 用官方的话讲:“RT-Thread 的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来,每个具体设备都可以继承其父类对象的属性,并派生出其私有属性。”

设备对象具体的定义如下

 通过以上描述,一系列的注册过程就是把硬件驱动的参数设置,功能函数绑定到具体的设备实例上,然后通过设备名字找到该设备实例,接着通过操作接口完成各种功能的使用。

PS:

第一次这么认真的写了如此篇幅的博客,只为积累一些学习知识点,如果有描述错误的地方希望指摘,定虚心学习。

版权声明:本文为CSDN博主「菜袅1号」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/flaycsdn/article/details/122339185

目录

 

1. 概述

2. 原理解析

3. 源码解析

3.1. 创建设备

3.2.  注册到驱动框架

3.3. 注册到IO设备管理器

 4. 小结

1. 概述

本着由简入繁的原则,分析源码以STM32平台的看门狗源码为例,正好参考官方资料辅助学习下。

硬件平台及软件版本如下:

硬件平台:STM32F407ZG

RT-Thread版本:4.0.4

在分析源码前需要了解的基础知识如下:

自动初始化机制

RT-Thread 文档中心

I/O驱动模型

RT-Thread 文档中心

看门狗驱动框架

RT-Thread 文档中心

2. 原理解析

通过基础知识的准备,切回到我们的主题。那么在I/O设备模型下,使用watchdog驱动框架如何驱动硬件看门狗呢?

看下官方资料给出的流程图:

通过这张框图明确的流程是

1,创建看门狗设备,并实现底层驱动

2,注册看门狗设备到看门狗设备驱动框架

3,注册I/O设备到I/O设备管理器

4,应用程序使用看门狗

3. 源码解析

原则上说,分析源码是要明确框架才能进一步分析,但是为了方便与简化理解,不妨从设备驱动开始,往上层一步一步追踪来研究源码。

几个关键的RT-Thread的代码目录

设备的驱动代码在

libraries/HAL_Drivers

驱动框架

rt-thread/components/drivers

设备模型

rt-thread/src

3.1. 创建设备

按照上面的流程图理解,首先是要创建看门狗设备,这时看驱动文件drv_wdt.c。

看门狗设备的结构体定义如下,该看门狗设备采用静态初始化的方法,定义了看门狗设备对象及看门狗设备的操作方法。

struct stm32_wdt_obj
{
    //看门狗设备定义
    rt_watchdog_t watchdog;
    //看门狗的硬件结构体定义
    IWDG_HandleTypeDef hiwdg;
    //是否初始化的标志
    rt_uint16_t is_start;
};
//看门狗实例
static struct stm32_wdt_obj stm32_wdt;
//看门狗的操作方法
static struct rt_watchdog_ops ops;

其中使用了看门狗驱动框架的看门狗相关的结构体定义,后面在说。硬件结构体,就是stm32官方的定义,这个可以去看官方驱动示例。那么结合结构体的定义,看门狗的初始化如下,主要是配置硬件参数,然后向驱动框架中注册该设备,名字即为“wdt”。

int rt_wdt_init(void)
{
    //看门狗硬件参数配置
#if defined(SOC_SERIES_STM32H7)
    stm32_wdt.hiwdg.Instance = IWDG1;
#else
    stm32_wdt.hiwdg.Instance = IWDG;
#endif
    stm32_wdt.hiwdg.Init.Prescaler = IWDG_PRESCALER_256;

    stm32_wdt.hiwdg.Init.Reload = 0x00000FFF;
#if defined(SOC_SERIES_STM32F0) || defined(SOC_SERIES_STM32L4) || defined(SOC_SERIES_STM32F7) \
    || defined(SOC_SERIES_STM32H7) || defined(SOC_SERIES_STM32L0)
    stm32_wdt.hiwdg.Init.Window = 0x00000FFF;
#endif
    stm32_wdt.is_start = 0;

    //操作方法的绑定
    ops.init = &wdt_init;
    ops.control = &wdt_control;
    stm32_wdt.watchdog.ops = &ops;

    //向驱动框架中注册该设备
    if (rt_hw_watchdog_register(&stm32_wdt.watchdog, "wdt", RT_DEVICE_FLAG_DEACTIVATE, RT_NULL) != RT_EOK)
    {
        LOG_E("wdt device register failed.");
        return -RT_ERROR;
    }
    LOG_D("wdt device register success.");
    return RT_EOK;
}
INIT_BOARD_EXPORT(rt_wdt_init);

 此处看门狗设备的创建采用自动初始化机制,按照自动初始化机制描述,该初始化是板级初始化。初始化相关宏如下:


接下来再看下,看门狗设备定义的驱动功能,就两个,一个是看门狗的初始化,另一个是看门狗的各种功能控制(喂狗、设置超时时间等)。此处的初始化是一个空函数,看门狗的初始化是通过

static rt_err_t wdt_control(rt_watchdog_t *wdt, int cmd, void *arg)

结合RT_DEVICE_CTRL_WDT_START命令来实现的。

static rt_err_t wdt_init(rt_watchdog_t *wdt)
{
    return RT_EOK;
}

static rt_err_t wdt_control(rt_watchdog_t *wdt, int cmd, void *arg)
{
    switch (cmd)
    {
        /* feed the watchdog */
    case RT_DEVICE_CTRL_WDT_KEEPALIVE:
        if(HAL_IWDG_Refresh(&stm32_wdt.hiwdg) != HAL_OK)
        {
            LOG_E("watch dog keepalive fail.");
        }
        break;
        /* set watchdog timeout */
    case RT_DEVICE_CTRL_WDT_SET_TIMEOUT:
#if defined(LSI_VALUE)
        if(LSI_VALUE)
        {
            stm32_wdt.hiwdg.Init.Reload = (*((rt_uint32_t*)arg)) * LSI_VALUE / 256 ;
        }
        else
        {
            LOG_E("Please define the value of LSI_VALUE!");
        }
        if(stm32_wdt.hiwdg.Init.Reload > 0xFFF)
        {
            LOG_E("wdg set timeout parameter too large, please less than %ds",0xFFF * 256 / LSI_VALUE);
            return -RT_EINVAL;
        }
#else
  #error "Please define the value of LSI_VALUE!"
#endif
        if(stm32_wdt.is_start)
        {
            if (HAL_IWDG_Init(&stm32_wdt.hiwdg) != HAL_OK)
            {
                LOG_E("wdg set timeout failed.");
                return -RT_ERROR;
            }
        }
        break;
    case RT_DEVICE_CTRL_WDT_GET_TIMEOUT:
#if defined(LSI_VALUE)
        if(LSI_VALUE)
        {
            (*((rt_uint32_t*)arg)) = stm32_wdt.hiwdg.Init.Reload * 256 / LSI_VALUE;
        }
        else
        {
            LOG_E("Please define the value of LSI_VALUE!");
        }
#else
  #error "Please define the value of LSI_VALUE!"
#endif
        break;
    case RT_DEVICE_CTRL_WDT_START:
        if (HAL_IWDG_Init(&stm32_wdt.hiwdg) != HAL_OK)
        {
            LOG_E("wdt start failed.");
            return -RT_ERROR;
        }
        stm32_wdt.is_start = 1;
        break;
    default:
        LOG_W("This command is not supported.");
        return -RT_ERROR;
    }
    return RT_EOK;
}

那么通过以上的源码就实现了硬件层面的看门狗的创建,或者说参数配置及功能实现。

接下来就是注册看门狗设备到看门狗设备框架了。

3.2.  注册到驱动框架

上一小节的的看门狗初始化中调用了注册函数,该函数即实现了注册到驱动框架的功能。

if (rt_hw_watchdog_register(&stm32_wdt.watchdog, "wdt", RT_DEVICE_FLAG_DEACTIVATE, RT_NULL) != RT_EOK)
{
    LOG_E("wdt device register failed.");
    return -RT_ERROR;
}

看门狗的驱动框架实现见watchdog.c,注册函数的实现如下:

rt_err_t rt_hw_watchdog_register(struct rt_watchdog_device *wtd,
                                 const char                *name,
                                 rt_uint32_t                flag,
                                 void                      *data)
{
    struct rt_device *device;
    RT_ASSERT(wtd != RT_NULL);

    device = &(wtd->parent);

    device->type        = RT_Device_Class_Security;
    //回调处理
    device->rx_indicate = RT_NULL;
    device->tx_complete = RT_NULL;

//看门狗设备的操作方法
#ifdef RT_USING_DEVICE_OPS
    device->ops         = &wdt_ops;
#else
    device->init        = rt_watchdog_init;
    device->open        = rt_watchdog_open;
    device->close       = rt_watchdog_close;
    device->read        = RT_NULL;
    device->write       = RT_NULL;
    device->control     = rt_watchdog_control;
#endif
    device->user_data   = data;

    /* register a character device */
    return rt_device_register(device, name, flag);
}

此处,就是把看门狗设备的类型、回调函数、操作方法设置或绑定好,由于看门狗设备操作简单,此处的回调直接为空。

其中RT_USING_DEVICE_OPS宏,个人理解是为了兼容不同版本的问题,除了ram和rom使用大小有区别,其他没有什么影响。

接下来看下看门狗框架中的设备操作方法的实现,按照官方的I/O设备模型需要实现如下操作方法。

 在看门狗设备中,由于不需要使用读写功能,则只定义了初始化、打开、关闭、控制的函数。如下的操作方法实现,最终都是调用上一小结中驱动功能的初始化功能控制来实现的。

static rt_err_t rt_watchdog_init(struct rt_device *dev)
{
    rt_watchdog_t *wtd;

    RT_ASSERT(dev != RT_NULL);
    wtd = (rt_watchdog_t *)dev;
    if (wtd->ops->init)
    {
        return (wtd->ops->init(wtd));
    }

    return (-RT_ENOSYS);
}

static rt_err_t rt_watchdog_open(struct rt_device *dev, rt_uint16_t oflag)
{
    return (RT_EOK);
}

static rt_err_t rt_watchdog_close(struct rt_device *dev)
{
    rt_watchdog_t *wtd;

    RT_ASSERT(dev != RT_NULL);
    wtd = (rt_watchdog_t *)dev;

    if (wtd->ops->control(wtd, RT_DEVICE_CTRL_WDT_STOP, RT_NULL) != RT_EOK)
    {
        rt_kprintf(" This watchdog can not be stoped\n");

        return (-RT_ERROR);
    }

    return (RT_EOK);
}

static rt_err_t rt_watchdog_control(struct rt_device *dev,
                                    int              cmd,
                                    void             *args)
{
    rt_watchdog_t *wtd;

    RT_ASSERT(dev != RT_NULL);
    wtd = (rt_watchdog_t *)dev;

    return (wtd->ops->control(wtd, cmd, args));
}

通过以上源码,就实现了看门狗设备注册到驱动框架中,接下来就是把它注册到IO设备管理器中。

3.3. 注册到IO设备管理器

注册到IO设备管理器的函数如下,在文件device.c中。

rt_device_register(device, name, flag);

 函数实现如下:

rt_err_t rt_device_register(rt_device_t dev,
                            const char *name,
                            rt_uint16_t flags)
{
    if (dev == RT_NULL)
        return -RT_ERROR;

    if (rt_device_find(name) != RT_NULL)
        return -RT_ERROR;

    rt_object_init(&(dev->parent), RT_Object_Class_Device, name);
    dev->flag = flags;
    dev->ref_count = 0;
    dev->open_flag = 0;

#ifdef RT_USING_POSIX
    dev->fops = RT_NULL;
    rt_wqueue_init(&(dev->wait_queue));
#endif /* RT_USING_POSIX */

    return RT_EOK;
}
RTM_EXPORT(rt_device_register);

到此为止,看门狗设备就被注册到IO设备管理器中了,可以使用IO设备管理接口操作看门狗设备了。

 4. 小结

总体来看,要明白这套驱动框架及模式,首先要理解整体流程,其次是RT-Thread的设备对象,所有的设备都是通过设备基类派生出来的,或者用结构体的方式理解就是具体的设备类结构体包含了设备基类的结构体。

 用官方的话讲:“RT-Thread 的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来,每个具体设备都可以继承其父类对象的属性,并派生出其私有属性。”

设备对象具体的定义如下

 通过以上描述,一系列的注册过程就是把硬件驱动的参数设置,功能函数绑定到具体的设备实例上,然后通过设备名字找到该设备实例,接着通过操作接口完成各种功能的使用。

PS:

第一次这么认真的写了如此篇幅的博客,只为积累一些学习知识点,如果有描述错误的地方希望指摘,定虚心学习。

版权声明:本文为CSDN博主「菜袅1号」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/flaycsdn/article/details/122339185

生成海报
点赞 0

菜袅1号

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

RT-Thread I/O设备模型及驱动框架解析(一)

目录 1. 概述 2. 原理解析 3. 源码解析 3.1. 创建设备 3.2.  注册到驱动框架 3.3. 注册到IO设备管理器 4. 小结 1. 概述 本着由简入繁的原则,分析源码以STM32平台的看门狗源码为例&#

舵机角度精确控制

导读:1.只想控制舵机不想了解其他的直接看 (三 四)2.会控制舵机但是角度控制的不精确的话可以直接看(四)想直接找参数不想调节舵机的朋友我劝你还是看一下这篇文章&#xff0