硬件测试平台:正点原子潘多拉STM32L4开发板
OS内核版本:4.0.0

注意:下面的示例代码是从原子提供的例程中摘录,因此可能与最新的RT-Thread源码有出入(因为RT-Thread源码在不断的开发维护中)
下面摘录的例程中,关键位置我给出了注释

本文涉及内容对初学者可能较为晦涩难懂,如只是想先简单学习Pin设备驱动的API使用可以参考:RT-Thread Pin设备驱动API应用介绍

首先看main.c,可见main函数主要实现了LED闪烁,以及打印LED状态的功能

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>/* using RED LED in RGB */
#define LED_PIN              PIN_LED_Rint 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_OPSconst 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;
#endifvoid                     *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接口层和底层连接起来呢?
答案是在初始化阶段里面实现了,关于RT-Thread的初始化流程可查看:RT-Thread启动流程
初始化流程中的调用关系如下:
$ Sub $ $main(void) =》 rtthread_startup() =》 rt_hw_board_init() =》 rt_hw_pin_init()
(对 $ Sub $ $不明白的可以参考:关于 $ Super $ $ 和 $ Sub $ $ 的用法)
前面的函数就不放出来了,直接从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个接口函数即可。

至此我们已初步理清了RT-Thread Pin设备驱动的框架关系了:

如上图,我们在应用层需要学会rt_pin_mode等6个API接口的使用,在底层需要实现GPIO驱动的回调函数。
从上图可以直观地看到 Pin 设备驱动框架和 GPIO 驱动是如果对接到一起的。
如上图所示:

  1. 左侧为 Pin 设备驱动框架封装的 API 接口,向上提供给应用层使用;
  2. 右侧为具体芯片平台对接 Pin 设备的驱动程序,与 Pin 设备框架提供的 API 接口一一对应,通过 rt_pin_ops 结构体关联到一起,驱动框架层接口最终会回调驱动层接口;
  3. 最后通过 rt_device_pin_register 函数接口,将底层驱动与 Pin 设备框架绑定到一起,注册到 RT-Thread 的设备框架中。

在对接 RT-Thread Pin 设备框架的时候,仅需要实现上图右侧 rt_pin_ops 结构体中的所有 callback 回调函数即可,然后通过 rt_device_pin_register 注册到系统,最后使用 rt_hw_pin_init 调用以进行初始化。
rt_hw_pin_init 在系统启动过程中被 rt_hw_board_init 调用。

点灯主要关注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                0x01rt_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();
看到struct pin_index类型第二个成员void (*rcc)(void);
pins是struct pin_index类型的结构体数组
我们用到的红色LED对应的__STM32_PIN(38, E, 7)
第二个宏参数E通过GPIO##gpio##_CLK_ENABLE ==》就变成了GPIOE_CLK_ENABLE
对##和#的用法不熟悉可参考: 详解C语言中 ##和# 的用法

通过设备驱动框架访问pin设备

  1. RTT设备驱动框架提供了统一的API:
API 说明
rt_err_t rt_device_init (rt_device_t dev) 设备初始化
rt_err_t rt_device_open (rt_device_t dev, rt_uint16_t oflag) 打开设备
rt_err_t rt_device_close(rt_device_t dev) 关闭设备
rt_size_t rt_device_read (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) 读设备
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) 写设备
rt_err_t rt_device_control(rt_device_t dev, int cmd, void *arg) 控制设备

RTT提供的设备驱动API使用了类似Linux的设计思路,对设备的访问都通过如下接口open,read,write,close等来完成操作。
其中,pin设备的接口关系如下:

目前我的这套代码中_pin_control()只实现了GPIO的模式设置,如果要使用这组API需要自己增加相关代码实现对中断的控制:

static rt_err_t  _pin_control(rt_device_t dev, int cmd, void *args)
{struct rt_device_pin_mode *mode;struct rt_device_pin *pin = (struct rt_device_pin *)dev;/* check parameters */RT_ASSERT(pin != RT_NULL);mode = (struct rt_device_pin_mode *) args;if (mode == RT_NULL) return -RT_ERROR;pin->ops->pin_mode(dev, (rt_base_t)mode->pin, (rt_base_t)mode->mode);return 0;
}

下面的示例代码从调用设备驱动框架的接口来实现对LED的控制

int main(void)
{int count = 1;struct rt_device_pin *pin_dev = RT_NULL;struct rt_device_pin_mode rt_device_pin_mode_led = {LED_PIN, PIN_MODE_OUTPUT};struct rt_device_pin_status rt_device_pin_status_led = {LED_PIN, PIN_HIGH};pin_dev = (struct rt_device_pin *)rt_device_find("pin");rt_device_open((rt_device_t)pin_dev, RT_DEVICE_OFLAG_RDWR);rt_device_control(&pin_dev->parent, 0, &rt_device_pin_mode_led);while (count++){rt_device_pin_status_led.status = PIN_HIGH;rt_device_write((rt_device_t)pin_dev, 0, &rt_device_pin_status_led, sizeof(rt_device_pin_status_led));rt_thread_mdelay(1000);rt_device_pin_status_led.status = PIN_LOW;rt_device_write((rt_device_t)pin_dev, 0, &rt_device_pin_status_led, sizeof(rt_device_pin_status_led));rt_thread_mdelay(1000);}return RT_EOK;
}

最后

唠一句:
RT-Thread的代码还是不错的,初学者可能会对这种分层思想有点懵,但是实际项目中,这种思想一定要运用起来,只有真正的去解耦合,把应用层和驱动层尽可能分开,才能把应用层的代码做到方便在不同平台移植,这可能试着把这种工程代码移植一下平台,再对比一下不分层,应用和驱动相互交错的工程移植一下,就明白到底这种编程思想强在哪里了。重复地造轮子只会让人越来越累,降低工作效率。

RT-Thread pin设备驱动代码结构剖析相关推荐

  1. RT-Thread uart串口设备驱动代码结构剖析

    硬件测试平台:正点原子潘多拉STM32L4开发板 OS内核版本:4.0.0 注意:下面的示例代码是从原子提供的例程中摘录,因此可能与最新的RT-Thread源码有出入(因为RT-Thread源码在不断 ...

  2. RT-Thread Pin设备驱动API应用介绍

    概要 本文主要涉及Pin驱动相关的API接口的简要介绍及使用示例,有兴趣深入了解Pin驱动程序框架可参考:RT-Thread pin设备驱动代码结构剖析 PIN设备的操作方法 应用程序通过RT-Thr ...

  3. Linux驱动编写(块设备驱动代码)

    [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 按照ldd的说法,linux的设备驱动包括了char,block,net三种设备.char设备 ...

  4. I2C适配器驱动及设备驱动代码详解

    实例Accelerometer Sensors: bma2x2 1.简述 根据i2C驱动的开发流程去分析i2c从机设备bma2x2(加速度sensors)的驱动:以及分析其挂载的控制器(适配器)i2c ...

  5. UE4 Chaos代码结构剖析

    一.代码结构 Chaos的核心代码存储在Source/Runtime/Experimental中 而一些之外的代码都是以插件的形式存在于引擎之中.InteractiveToolsFramework并不 ...

  6. 基于GD32F103C8T6添加RT Thread nano设备框架并添加串口设备(以控制台console( uart0 )为例)

    最近没事琢磨了一下使用设备框架的问题.因为将串口注册到设备框架可以应用十分丰富的软件包. 于是就整理了一下手上的工程,重新将工程梳理了一遍. 像这样是十分清爽了,其中RTOS是操作系统源代码 并且学习 ...

  7. 字符设备驱动代码完整分析

    1.编译.安装驱动程序 Linux系统中,驱动程序通常采用内核模块的程序结构来进行编码.因此编译.安装驱动程序实质是编译.安装内核模块 memdev.c #include <linux/modu ...

  8. 高通平台--camera驱动代码结构

    1.camx/src/core sensor.manager.pipeline等结构的实现 camxpipeline.h /// @brief Pipeline create input data   ...

  9. Linux设备驱动之UART驱动结构

    一.对于串口驱动Linux系统中UART驱动属于终端设备驱动,应该说是实现串口驱动和终端驱动来实现串口终端设备的驱动.要了解串口终端的驱动在Linux系统的结构就先要了解终端设备驱动在Linux系统中 ...

最新文章

  1. 你的产品没有一炮而红,后面该怎么办?
  2. 13-Qt6 QMap
  3. linux mysql 实战_linux实用实战
  4. OpenShift 4 - Pod 优先级
  5. protocol学习笔记001---RPC和HTTP协议之间的区别_与各自优势
  6. 2021浙江高考成绩排名查询,2021年浙江高考成绩排名查询,第一批分数线23日公布...
  7. Vue.js(2.x)之插值
  8. Python的Numpy库简述
  9. 白帽子讲Web安全(第 10 章 访问控制)
  10. PCAN-Explorer5 基本使用方法
  11. Android自定义View之绘制圆形头像
  12. Axure原型|天猫、京东、拉钩等网站banner原型分享
  13. 阿里云 ECS windows服务器创建+部署+域名
  14. Espresso的详细使用
  15. 外贸老鸟帮新人点评、修改的5个开发信案例
  16. CAD二次开发 根据多段线Polyline产生的线段Line需要做进一步处理才可以使用
  17. Excel 自动生成排名 RANK函数的使用
  18. VMware 只能打开一个.vmx,无法打开第二个
  19. 遥感理论基础——电磁波(一)
  20. Allegro中封装库中的封装更新到PCB中出现焊盘中心错位

热门文章

  1. WebRTC/Chromium在2020年的更新
  2. 由STGW下载慢问题引发的网络传输学习之旅
  3. 长文 | 腾讯提出 AI For FEW 构想,呼吁人工智能为地球思考
  4. 论亚马逊QLDB与腾讯TDSQL对历史数据的管理和计算
  5. 解读直播连麦与点播加密
  6. Nginx虚拟目录alias和root目录
  7. 互斥锁属性PTHREAD_MUTEX_RECURSIVE
  8. 俞敏洪:如果创业者缺乏这8种能力,失败可能性很大
  9. AliOS Things v1.1.1新特性
  10. Android O 迁移应用官方指南