1. 程式人生 > 其它 >RT-Thread pin裝置驅動程式碼結構剖析

RT-Thread pin裝置驅動程式碼結構剖析

技術標籤:RT-Thread進階系列嵌入式微控制器

硬體測試平臺:正點原子潘多拉STM32L4開發板
核心版本:4.0.0

注意:下面的示例程式碼是從原子提供的例程中摘錄,因此可能與最新的RT-Thread原始碼有出入(因為RT-Thread原始碼在不斷的開發維護中)

首先看main.c,可見main函式主要實現了LED閃爍,以及列印LED狀態的功能

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>

/* using RED LED in RGB */
#define LED_PIN              PIN_LED_R
int main(void) { unsigned int count = 1; /* set LED pin mode to output */ rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); while (count > 0) { /* led on */ rt_pin_write(LED_PIN, PIN_LOW); rt_kprintf("led on, count: %d\n", count); rt_thread_mdelay(
500); /* led off */ rt_pin_write(LED_PIN, PIN_HIGH); rt_kprintf("led off\n"); rt_thread_mdelay(500); count++; } return 0; }

PIN_LED_R在硬體驅動層的drv_gpio.h中定義了

#define PIN_LED_R     38        // PE7 :  LED_R        --> LED

剖析順序從上到下,從應用層深入到驅動層。(pin驅動相關的原始檔主要包括drv_gpio.c 、pin.c、 device.c)

程式碼框架如下圖:
在這裡插入圖片描述
介面層的pin.c往上對接使用者,往下對接底層驅動。
對於不同晶片,使用者層的介面是統一的,而對於驅動層來說,只需要對接好相應的回撥函式。
通過統一的介面,應用開發不需要知道底層驅動,減少重複造輪子的時間。

按照點燈裸機的程式設計思路,先是開啟GPIO時鐘,然後初始化控制LED的GPIO為輸出,最後寫GPIO輸出高或低電平,main函式中先是rt_pin_mode函式,從字面上看也知道這是設定pin工作模式。下面追蹤程式碼:

/* RT-Thread Hardware PIN APIs */
void rt_pin_mode(rt_base_t pin, rt_base_t mode)
{
    RT_ASSERT(_hw_pin.ops != RT_NULL); //斷言 檢查_hw_pin.ops不為空
    _hw_pin.ops->pin_mode(&_hw_pin.parent, pin, mode);
}

結構體_hw_pin 定義在pin.c中
static struct rt_device_pin _hw_pin;
追蹤struct rt_device_pin 這個型別

/* pin device and operations for RT-Thread */
struct rt_device_pin
{
    struct rt_device parent;
    const struct rt_pin_ops *ops;
};

//在rtdef.h
/**
 * Device structure
 */
struct rt_device
{
    struct rt_object          parent;                   /**< inherit from rt_object */

    enum rt_device_class_type type;                     /**< device type */
    rt_uint16_t               flag;                     /**< device flag */
    rt_uint16_t               open_flag;                /**< device open flag */

    rt_uint8_t                ref_count;                /**< reference count */
    rt_uint8_t                device_id;                /**< 0 - 255 */

    /* device call back */
    rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
    rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);

#ifdef RT_USING_DEVICE_OPS
    const struct rt_device_ops *ops;
#else
    /* common device interface */
    rt_err_t  (*init)   (rt_device_t dev);
    rt_err_t  (*open)   (rt_device_t dev, rt_uint16_t oflag);
    rt_err_t  (*close)  (rt_device_t dev);
    rt_size_t (*read)   (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
    rt_size_t (*write)  (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
    rt_err_t  (*control)(rt_device_t dev, int cmd, void *args);
#endif

#if defined(RT_USING_POSIX)
    const struct dfs_file_ops *fops;
    struct rt_wqueue wait_queue;
#endif

    void                     *user_data;                /**< device private data */
};

//在pin.h
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);

    /* TODO: add GPIO interrupt */
    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);
};

struct rt_device_pin 這個型別中的成員ops,是介面層與硬體驅動層的媒介。
從struct rt_pin_ops型別可以看到裡面是六個函式指標分別對應設定pin模式,寫pin,讀pin,以及三個與中斷有關的。

那問題是,在哪裡把ops變數初始化了,也就是把pin介面層和底層連線起來呢?
在初始化階段裡面實現了
$ Sub $ $main(void) -> rtthread_startup() -> rt_hw_board_init() -> rt_hw_pin_init()

//這是drv_gpio.c的
const static struct rt_pin_ops _stm32_pin_ops =
{
    stm32_pin_mode,
    stm32_pin_write,
    stm32_pin_read,
    stm32_pin_attach_irq,
    stm32_pin_dettach_irq,
    stm32_pin_irq_enable,
};
int rt_hw_pin_init(void)
{
    return rt_device_pin_register("pin", &_stm32_pin_ops, RT_NULL);
}

//這是pin.c的
int rt_device_pin_register(const char *name, const struct rt_pin_ops *ops, void *user_data)
{
    _hw_pin.parent.type         = RT_Device_Class_Miscellaneous;
    _hw_pin.parent.rx_indicate  = RT_NULL;
    _hw_pin.parent.tx_complete  = RT_NULL;

#ifdef RT_USING_DEVICE_OPS
    _hw_pin.parent.ops          = &pin_ops;
#else
    _hw_pin.parent.init         = RT_NULL;
    _hw_pin.parent.open         = RT_NULL;
    _hw_pin.parent.close        = RT_NULL;
    _hw_pin.parent.read         = _pin_read;
    _hw_pin.parent.write        = _pin_write;
    _hw_pin.parent.control      = _pin_control;
#endif

    _hw_pin.ops                 = ops;   //這裡把_stm32_pin_ops和 _hw_pin.ops 連線起來了
    _hw_pin.parent.user_data    = user_data;

    /* register a character device */
    rt_device_register(&_hw_pin.parent, name, RT_DEVICE_FLAG_RDWR);

    return 0;
}

從上面的程式碼可以看出,底層驅動只要實現_stm32_pin_ops 裡的6個介面函式即可。
點燈主要關注stm32_pin_mode和stm32_pin_write這兩個函式:(這都是在驅動層也就是drv_gpio.c中)

static void stm32_pin_mode(rt_device_t dev, rt_base_t pin, rt_base_t mode)
{
    const struct pin_index *index;
    GPIO_InitTypeDef GPIO_InitStruct;

    index = get_pin(pin);
    if (index == RT_NULL)
    {
        return;
    }

    /* GPIO Periph clock enable */
    index->rcc();

    /* Configure GPIO_InitStructure */
    GPIO_InitStruct.Pin = index->pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

    if (mode == PIN_MODE_OUTPUT)
    {
        /* output setting */
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
    }
    else if (mode == PIN_MODE_INPUT)
    {
        /* input setting: not pull. */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
    }
    else if (mode == PIN_MODE_INPUT_PULLUP)
    {
        /* input setting: pull up. */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
    }
    else if (mode == PIN_MODE_INPUT_PULLDOWN)
    {
        /* input setting: pull down. */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
        GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    }
    else if (mode == PIN_MODE_OUTPUT_OD)
    {
        /* output setting: od. */
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
    }

    HAL_GPIO_Init(index->gpio, &GPIO_InitStruct);
}

static void stm32_pin_write(rt_device_t dev, rt_base_t pin, rt_base_t value)
{
    const struct pin_index *index;

    index = get_pin(pin);
    if (index == RT_NULL)
    {
        return;
    }

    HAL_GPIO_WritePin(index->gpio, index->pin, (GPIO_PinState)value);//呼叫HAL庫函式控制GPIO輸出高低電平
}

關於stm32_pin_mode函式有一個問題,我們知道GPIO_InitStruct需要初始化四個成員變數分別是選擇pin,選擇GPIO模式,選擇是否加上下拉,選擇GPIO速度,上述程式碼從對上層的通用性考慮(不是每個晶片都可以控制速率等),只往上提供了mode,而速度固定在了GPIO_SPEED_FREQ_HIGH,上拉下拉則根據mode固定變化。如果有特殊需求對某個GPIO要做一些特殊配置,比如要降低某個GPIO的速率以降低功耗,這就得另外去改了。

關於stm32_pin_write()函式單獨拉出來看一下:


#define LED_PIN              PIN_LED_R
#define PIN_LED_R     38        // PE7 :  LED_R        --> LED

#define PIN_LOW                 0x00
#define PIN_HIGH                0x01

rt_pin_write(LED_PIN, PIN_LOW);

void rt_pin_write(rt_base_t pin, rt_base_t value)
{
    RT_ASSERT(_hw_pin.ops != RT_NULL);
    _hw_pin.ops->pin_write(&_hw_pin.parent, pin, value); //以上分析我們知道,pin_write實際上就是指向了stm32_pin_write函式
}

static void stm32_pin_write(rt_device_t dev, rt_base_t pin, rt_base_t value)
{
    const struct pin_index *index;

    index = get_pin(pin);
    if (index == RT_NULL)
    {
        return;
    }

    HAL_GPIO_WritePin(index->gpio, index->pin, (GPIO_PinState)value);//呼叫HAL庫函式控制GPIO輸出高低電平
}

static void stm32_pin_write(rt_device_t dev, rt_base_t pin, rt_base_t value)
{
    const struct pin_index *index;

    index = get_pin(pin);
    if (index == RT_NULL)
    {
        return;
    }

    HAL_GPIO_WritePin(index->gpio, index->pin, (GPIO_PinState)value);//(GPIO_PinState)這裡用了強制轉換是防止上層傳下來0或1會編譯報警
}

#define ITEM_NUM(items) sizeof(items) / sizeof(items[0])
static const struct pin_index *get_pin(uint8_t pin)
{
    const struct pin_index *index;

    if (pin < ITEM_NUM(pins))
    {
        index = &pins[pin];
        if (index->index == -1)
            index = RT_NULL;
    }
    else
    {
        index = RT_NULL;
    }

    return index;
};

static const struct pin_index pins[] =
{
    __STM32_PIN_DEFAULT,
    __STM32_PIN(1, E, 2),       // PE2 :  SAI1_MCLK_A  --> ES8388
    __STM32_PIN(2, E, 3),       // PE3 :  SAI1_SD_B    --> ES8388
    ...//省略
    __STM32_PIN(38, E, 7),      // PE7 :  LED_R        --> LED   //這是我們要用到的紅色LED腳
    ...//省略
    __STM32_PIN(98, E, 1),      // PE1 :  IO_PE1       --> EXTERNAL MODULE
    __STM32_PIN_DEFAULT,        //     :  VSS
    __STM32_PIN_DEFAULT,        //     :  VDD
};

/* STM32 GPIO driver */
struct pin_index
{
    int index;
    void (*rcc)(void);
    GPIO_TypeDef *gpio;
    uint32_t pin;
};

//這裡用到了##連線符  這個符號在RT-Thread裡用得很多
#define __STM32_PIN(index, gpio, gpio_index)                                \
    {                                                                       \
        index, GPIO##gpio##_CLK_ENABLE, GPIO##gpio, GPIO_PIN_##gpio_index   \
    }

呼叫rt_pin_write時第一個引數傳入38實際上就在struct pin_index pins[] 這裡面索引到__STM32_PIN(38, E, 7),
檢視原理圖發現,這個表對應了晶片的引腳序號,比如38腳就是PE7,也就是我們要用的紅色LED控制腳
在這裡插入圖片描述
rt_pin_write(38, 1)實際到了底層就是HAL_GPIO_WritePin(GPIOE, GPIO_PIN_7, (GPIO_PinState)1);
//(GPIO_PinState)這裡用了強制轉換是防止上層傳下來0或1會編譯報警 細節到位

還剩一個問題,在哪裡使能了該GPIO時鐘?
在stm32_pin_mode函式裡面使能了對應的GPIO時鐘,
/* GPIO Periph clock enable */
index->rcc();
rcc是一個函式指標,實際上是執行了 GPIOE_CLK_ENABLE();

全部分析完畢,寫完這篇文章大概用時兩個小時。
嘮一句:
RT-Thread的程式碼還是不錯的,初學者可能會對這種分層思想有點懵,但是實際專案中,這種思想一定要運用起來,只有真正的去解耦合,把應用層和驅動層儘可能分開,才能把應用層的程式碼做到方便在不同平臺移植,這可能把這種工程程式碼移植一下平臺,再對比一下不分層,應用和驅動相互交錯的工程移植一下,就明白到底這種程式設計思想強在哪裡了。重複地造輪子只會讓你越來越累,降低工作效率。