RT-Thread设备框架之pin源码分析
有关 pin 设备的共有4个文件,分别是
pin.c、pin.h、drv_gpio.c、drv_gpio.h。
其中pin.c、pin.h是上层文件,为用户提供可以直接调用的API。而drv_gpio.c、drv_gpio.h是底层的驱动文件,实现对接HAL库的功能(或者其他芯片厂商的SDK),是真正“干活”的那一个。
这样做的好处就是上层能对各平台驱动实现抽象,调用一套接口就能操作不同平台的硬件,代码复用性和可移植性得到了大大的提升。既然drv_gpio.c.h是真正“干活”的,那么我们分析源码自然就先从这里开始。
注:本文基于4.0.2版而写,不同版本的源码位置可能不一样。
drv_gpio.h的解读
1、
#define __STM32_PORT(port) GPIO##port##_BASE
要知道宏定义的本质是字符替换,但有些特殊符号不会被替换,有着对应的特殊含义。比方说这里的##,就是连接的意思(字面上的连接)。打个比方,__STM32_PORT(A)就是GPIOA_BASE的意思。而GPIOA_BASE是什么意思,用来干什么的,相信对库函数有了解的朋友会知道。
2、
#define GET_PIN(PORTX, PIN) (rt_base_t)((16 * ( ((rt_base_t)__STM32_PORT(PORTx) - (rt_base_t)GPIOA_BASE)/(0x0400UL) )) + PIN)
这个宏在我们上层注册IO口时会用到,看起来十分复杂,但只要了解它的作用,相信大家都能看得懂。
它的作用是将IO口的引脚号(如PA5、PC13这样的)转化成IO口的索引号(如1、2、3、4、5这样的)。编号表在drv_gpio.c文件中的第20行,编排的规则是什么样的相信大家看到代码就能明白。至于为什么要给每个引脚都编个号,我们待会再来解释。下面解释一下这个宏的实现原理,如果暂时不想看也可以跳过。
比如我们要注册PB9引脚,那么根据drv_gpio.c文件中的编号表可知它的编号是25。展开看一下,__STM32_PORT(B)就是GPIOB_BASE,GPIOB_BASE就等于0X40020400,GPIOA_BASE等于0X40020000。那么 16*(40020400 - 40020000)/400 + 9 = 16 +9 = 25 ,讲得不太好不知道大家看明白了没有>_<。
3、
#define __STM32_PIN(index, gpio, gpio_index) \
{ \
index, GPIO##gpio, GPIO_PIN_##gpio_index \
}
这个宏定义在drv_gpio.h中的第21行,是用来为引脚注册编号用的(编号表在drv_gpio.c文件中的第20行)。
如__STM32_PIN(8 , A, 8 )就等于:
{
8, GPIOA, GPIO_PIN_8
}
定义这个有什么用?当然是为下面这类结构体的成员变量赋值啊。
4、
struct pin_index
{
int index;
GPIO_TypeDef *gpio;
uint32_t pin;
};
这个结构体类型定义在drv_gpio.h中的第32行,里面的成员是不是就和上一个宏的内容对应上了。
drv_gpio.c文件的解读
1、
static const struct pin_index pins[];
这是个结构体数组,定义在drv_gpio.c文件中的第17行,相信大家已经不是第一次见到它了。结构体数组就是把n个相同类型的结构体打包成一个数组,每一个数组成员都是一个相同类型的结构体。在这里,每一个结构体都代表着一个引脚,而这个结构体数组,便囊括了所有的引脚。
我们看一下这个数组里面的内容,里面就有很多我们之前讲过的那个宏:
static const struct pin_index pins[] =
{
#if defined(GPIOA)
__STM32_PIN(0 , A, 0 ),
__STM32_PIN(1 , A, 1 ),
__STM32_PIN(2 , A, 2 ),
__STM32_PIN(3 , A, 3 ),
__STM32_PIN(4 , A, 4 ),
__STM32_PIN(5 , A, 5 ),
.
.
.
它可以等价成这一种形式:
static const struct pin_index pins[] =
{
#if defined(GPIOA)
{ 0, GPIOA, GPIO_PIN_0},
{ 1, GPIOA, GPIO_PIN_1},
{ 2, GPIOA, GPIO_PIN_2},
{ 3, GPIOA, GPIO_PIN_3},
{ 4, GPIOA, GPIO_PIN_4},
{ 5, GPIOA, GPIO_PIN_5},
.
.
.
因为想要定位到STM32某个GPIO需要两个参数,一个是端口号(如A、B、C),一个是引脚号(如1、2、3)。现在我们人为给它编写了一套索引,每个GPIO都对应一个唯一数字,之后想要定位到某个GPIO就只需要一个参数了(索引号)。
这里可能会有人疑惑,为什么不直接用下面这种写法,而是像上一种写法这样,先弄一个宏定义再包含进去。这里主要是涉及到分层的思想,因为这套设备框架不是专门为STM32定制的,而是要兼容市面上绝大多数的芯片。有些厂商的芯片,GPIO的端口号可能是数字而不是字母,譬如P1.0。
2、
static const struct pin_index *get_pin(uint8_t pin);
这是个函数,在drv_gpio.c文件中的第280行。输入的是索引号,返回的是对应索引号的那个结构体。作用是根据索引号,得到索引号对应GPIO的端口号及引脚号。
3、
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);
}
这个函数在第298行,用来拉高、拉低某个GPIO口。其中输入参数的pin是之前多次提到的索引号,value决定引脚的 高低电平,dev在这里没有用到。
当系统调用这个函数时,首先通过get_pin函数将索引号转化为对应的结构体,然后调用HAL库函数,将结构体中的成员作为参数输入进去。
假如索引号是3,对应的结构体就应该是这样:
pins[3] = { 3, GPIOA, GPIO_PIN_3};
所以调用HAL库函数就可以等价成这样:
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, (GPIO_PinState)value);
怎么样,是不是很神奇。
对drv_gpio文件的解读就到此为止了,其中还有很多关于引脚初始化、读引脚、设置外部中断的功能,在这里就不去分析了。感兴趣的朋友可以自行分析,无外乎就是间接调用HAL库了。到了这里,相信大家也知道这套框架该怎么对接标准库,以及其他芯片的SDK了。细心的朋友也许还能发现通用的框架相比专用的SDK会有什么局限性。
pin文件的解读
之前已经了解了
drv_gpio是如何对接HAL库的,接下来我们简单的讲一下pin是如何对drv_gpio进行抽象的
1、首先定位到drv_gpio.c中的第604行。
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,
};
这个结构体里面包含了6个函数指针,分别指向drv_gpio.c文件中定义的几个函数:设置引脚模式、写引脚、读引脚、引脚添加外部中断、引脚脱离中断、引脚中断使能。
这个结构体的类型定义在
pin.h文件中的第66行:
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);
};
虽然各厂商芯片有关GPIO的功能及SDK会有所不同,但无外乎也就这几样功能。RT-Thread这套pin设备框架为我们提供了良好的通用接口,降低我们换芯片后的学习成本。按照这些函数指针的类型编写相应的驱动函数,就可以实现对接。上层会通过访问这些函数指针来间接地调用底层的驱动函数,是不是有点像虚函数表。
2、再看drv_gpio.c的第783行。
return rt_device_pin_register("pin", &_stm32_pin_ops, RT_NULL);
这个函数的定义在
pin.c的77行:
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;
_hw_pin.parent.user_data = user_data;
/* register a character device */
rt_device_register(&_hw_pin.parent, name, RT_DEVICE_FLAG_RDWR);
return 0;
}
乍一看好像很复杂,做了很多事情,但实际上我们只需要看_hw_pin.ops = ops;这一句。它让_hw_pin结构体中ops这个结构体指针成员指向先前定义的_stm32_pin_ops结构体。之后上层的API就会访问这个结构体成员,再访问结构体成员里的函数指针。_hw_pin结构体定义在pin.c的第17行。
3、最后看pin.c文件中的第141行。
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);
}
这就是我们上层调用的API,写引脚函数。函数里面第一句是断言,防止用户输入不当的参数。第二句就是在访问前面说的函数指针了,说的复杂一点就是通过访问该结构体中的ops结构体指针成员来间接访问_stm32_pin_ops结构体中的函数指针成员来间接调用底层的驱动函数。套娃套了好几层,不知道大家晕了没有>_<。
其他几个函数的实现原理都是类似的,这里就懒得分析了。
内部调用流程
1、定义#define LED_PIN GET_PIN(B, 1)
用来把LED_PIN定义成某个引脚的索引号,之后便可以拿着LED_PIN去操作这个引脚了。GET_PIN(B, 1)用来得到PB0的索引号,这里等价于12。当然什么都不去定义,就拿着 12 去调用API操作PB0也是没有问题的。只是代码是给人看的,用这个宏定义可以提高代码可读性。
2、调用void rt_pin_write(rt_base_t pin, rt_base_t value)函数
通过一系列的操作,最终访问了_stm32_pin_ops结构体中的
void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_base_t value);函数指针,该指针指向
static void stm32_pin_write(rt_device_t dev, rt_base_t pin, rt_base_t value);函数。
3、调用static void stm32_pin_write(rt_device_t dev, rt_base_t pin, rt_base_t value);函数
调用get_pin函数,根据输入的索引号得到该索引号对应GPIO的端口号和引脚号,并最终调用HAL库。
总结
以上就是我对 pin 设备框架部分源码的解读。为什么是部分,一方面是个人时间精力不足。另一方面也担心篇幅太多会让人难以阅读,抓不住重点,所以目前就讲这么多。希望这篇文章能让大家体会到分层的思想,以及加深对结构体、指针等概念的理解。
这是本人发表的第一篇博文,有什么错误的地方也恳请大家指正,谢谢!(最后吐槽一下,CSDN的Markdown编辑器可真够恶心的)