ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zephyr应用: 17-Driver

Zephyr应用: 17-Driver 很好我们进入第 17 课DriverZephyr 驱动开发。结合你的学习路线已经完成 HelloWorld、GPIO、Button、UART、Kconfig、DeviceTree、Thread、Semaphore、Work Queue 等现在已经具备学习 Driver 所需的全部基础。摘要本课带你从零开始掌握 Zephyr 驱动开发。你将理解 Zephyr Driver Framework 与 Device Model 的核心概念学会使用DEVICE_DEFINE()注册驱动、通过 DeviceTree 创建设备并最终独立编写一个可编译运行的最小 Zephyr Driver打通从 Application 层到 Hardware 层的完整链路。第 17 课DriverZephyr Driver学习目标完成本课以后你应该能够✅ 理解 Zephyr Driver Framework✅ 理解 Device Model✅ 自己写一个 Driver✅ 使用 DeviceTree 创建设备✅ 编译成真正的 Zephyr Driver一、Driver 在 Zephyr 中的位置前面我们直接操作 GPIOgpio_pin_set_dt(led,1);实际上渲染错误:Mermaid 渲染失败: Lexical error on line 6. Unrecognized text. ...Hardware 硬件]所以mermaidflowch ----------------------^Zephyr 希望所有硬件都遵循这一分层结构。Driver Framework 提供统一的 Device Model让 Application 无需关心具体 Hardware从而实现硬件抽象和可移植性。二、Zephyr Driver Framework几乎所有 Driver 都遵循Application │ device_get_binding()│ ▼ Device Object │ API │ Driver │ HardwareDevice 是一个对象struct device例如UART SPI I2C ADC PWM Flash GPIO下面用一个表格对比两种最常见的驱动注册方式DEVICE_DEFINE()与DEVICE_DT_DEFINE()。对比项DEVICE_DEFINE()DEVICE_DT_DEFINE()适用场景纯软件设备、无 Devicetree 节点的设备如虚拟设备、测试桩有对应 Devicetree 节点的真实硬件设备如 UART、GPIO、传感器参数差异需要手动传入设备名、名称字符串、数据/配置指针等全部 9 个参数只需传入 Devicetree 节点标识符DEVICE_DT_GET可自动获取数据/配置指针通过DEVICE_DT_DEFINE自动关联DeviceTree 依赖程度完全不依赖 Devicetree设备实例由代码直接定义强依赖 Devicetree设备实例由 Devicetree 节点生成节点必须存在且 compatible 匹配使用示例DEVICE_DEFINE(mydev, MY_DEVICE, my_init, NULL, my_data, my_cfg, POST_KERNEL, 50, api);DEVICE_DT_DEFINE(mydev, DT_NODELABEL(mydev), my_init, NULL, my_data, my_cfg, POST_KERNEL, 50, api);简单来说如果你的设备在 Devicetree 中有对应节点比如板载 LED、UART 外设优先用DEVICE_DT_DEFINE()如果是纯软件逻辑、没有硬件节点就用DEVICE_DEFINE()。全部都是struct device三、Device 是什么例如conststructdevice\*uartDEVICE_DT_GET(DT_NODELABEL(uart0));这里uart其实就是struct device里面保存了Driver API Driver Data Configuration Init Function Name StateZephyr 在启动时会自动初始化这些设备对象。四、Driver 的组成一个 Driver 一般包含driver.c driver.h Kconfig CMakeLists.txt Devicetree Binding(.yaml)Devicetree Overlay(.overlay)真正驱动开发时driver/ sensor/ gpio/ uart/ spi/全部都是这种结构。五、一个 Driver 的最小结构例如structmy_data{intvalue;};structmy_config{uint32_tbase;};staticintmy_init(conststructdevice*dev){return0;}然后注册DEVICE_DEFINE(...)六、DEVICE_DEFINE()这是 Driver 开发中最重要的宏。例如DEVICE_DEFINE(mydev,MY_DEVICE, my_init, NULL,my_data,my_cfg, POST_KERNEL,50, NULL);它完成创建 device 保存 config 保存 data 保存 init 加入 device list启动时Kernel Boot ↓ DEVICE_DEFINE()↓ my_init()↓ Device Ready七、Application 如何访问 Driver例如const struct device\*devDEVICE_GET(mydev);或者DEVICE_DT_GET(...)然后device_is_ready(dev);如果true说明 Driver 初始化成功。八、Driver API真正的 Driver 会定义自己的 API。例如struct my_driver_api{int(*set)(const struct device *, int);int(*get)(const struct device *, int *);};然后staticconststructmy_driver_apiapi{.setmy_set,.getmy_get,};最后DEVICE_DEFINE(...,api);这样 Application↓ API ↓ Driver ↓ Hardware实现了解耦。九、Driver Data每个设备都有自己的运行数据struct my_data{int status;int count;boolenable;};Driver 中struct my_data\*datadev-data;即可访问。十、Driver Config配置在运行期间一般不会改变例如Base Address IRQ GPIO Pin Clock Frequency放在struct my_config{uint32_t base;int irq;};读取ApplicationDeviceKernelApplicationDeviceKernelPRE_KERNEL_1 / PRE_KERNEL_2PRE_KERNEL_1 / PRE_KERNEL_2POST_KERNELPOST_KERNELAPPLICATION(可选) PM Suspend 挂起Resume 恢复Boot 启动Device Create 创建设备对象Init 调用 my_init()返回 0Ready 设备就绪Application Use 调用驱动 API十一、Driver 初始化初始化函数static int my_init(const struct device *dev){printk(Driver Init\\n);return0;}启动Kernel Boot ↓ PRE_KERNEL ↓ POST_KERNEL ↓ APPLICATION可以指定 Driver 在哪个阶段初始化。十二、Driver 生命周期Boot ↓ Device Create ↓ Init ↓ Ready ↓ Application Use ↓(Optional PM Suspend)↓ Resume十三、Driver 与 DeviceTreeDriver 不直接写GPIO1 UART2 SPI3而是DeviceTree例如myled: led0{gpioslt;gpio013GPIO_ACTIVE_HIGHgt;;};Driver读取 DT ↓ 获得 GPIO ↓ 初始化十四、本课实验我们将自己写一个最小 Driver。目录17-Driver/ src/ main.c my_driver.c my_driver.h CMakeLists.txt prj.conf1. my_driver.h —— 驱动头文件#ifndefMY_DRIVER_H#defineMY_DRIVER_H#includezephyr/device.h#includezephyr/kernel.h/* 驱动 API 结构体 */structmy_driver_api{int(*set)(conststructdevice*dev,intvalue);int(*get)(conststructdevice*dev,int*value);};/* 提供给 Application 调用的接口 */intmy_driver_set(conststructdevice*dev,intvalue);intmy_driver_get(conststructdevice*dev,int*value);#endif/* MY_DRIVER_H */2. my_driver.c —— 驱动实现#includezephyr/kernel.h#includezephyr/device.h#includezephyr/sys/printk.h#includemy_driver.h/* 设备运行数据 */structmy_data{intstatus;intcount;bool enable;};/* 设备配置一般不变 */structmy_config{uint32_tbase;intirq;};/* 初始化函数系统启动时自动调用 */staticintmy_init(conststructdevice*dev){structmy_data*datadev-data;printk(Driver Init\n);data-status0;data-count0;data-enabletrue;return0;}/* API 实现set */staticintmy_set(conststructdevice*dev,intvalue){structmy_data*datadev-data;data-statusvalue;data-count;printk(Driver set: %d (count%d)\n,value,data-count);return0;}/* API 实现get */staticintmy_get(conststructdevice*dev,int*value){structmy_data*datadev-data;*valuedata-status;return0;}/* 绑定 API 到驱动 */staticconststructmy_driver_apiapi{.setmy_set,.getmy_get,};/* 静态实例 */staticstructmy_datamy_data;staticconststructmy_configmy_cfg{.base0x40000000,.irq5,};/* 注册驱动 */DEVICE_DEFINE(mydev,MY_DEVICE,my_init,NULL,my_data,my_cfg,POST_KERNEL,50,api);3. main.c —— Application 调用驱动#includezephyr/kernel.h#includezephyr/device.h#includezephyr/sys/printk.h#includemy_driver.hvoidmain(void){conststructdevice*dev;intvalue;printk(Application start\n);/* 获取设备对象 */devDEVICE_GET(mydev);/* 检查驱动是否初始化成功 */if(!device_is_ready(dev)){printk(Driver not ready!\n);return;}/* 调用驱动 API */my_driver_set(dev,42);my_driver_get(dev,value);printk(Application got: %d\n,value);}4. 编译运行结果*** Booting Zephyr OS build v3.7.0 *** Driver Init Application start Driver set:42(count1)Application got:42运行流程Boot ↓ Driver Init ↓ Application ↓ Call Driver API ↓ Driver prints十五、常见错误与排查初学者在编写 Zephyr Driver 时最容易在编译或运行阶段遇到下面几类问题。下面逐一给出典型报错、原因分析和解决方法。1. DEVICE_DEFINE 参数不匹配典型报错error: macroDEVICE_DEFINEpassed8arguments, but takes just9原因分析DEVICE_DEFINE()一共需要 9 个参数设备名、设备名称字符串、初始化函数、PM 回调、数据指针、配置指针、初始化级别、优先级、API 指针。初学者经常漏掉中间的NULLPM 回调或最后的api导致参数个数不对。解决方法对照完整签名逐项核对确保 9 个参数一个不少DEVICE_DEFINE(mydev,MY_DEVICE,my_init,NULL,my_data,my_cfg,POST_KERNEL,50,api);2. device_is_ready 返回 false典型报错Driver not ready!原因分析device_is_ready(dev)返回false通常有三种可能初始化函数my_init()返回了非 0 值Zephyr 认为设备初始化失败DEVICE_DEFINE中传入的my_data或my_cfg是未定义的变量导致设备对象无效初始化级别或优先级设置不当设备在 Application 访问时尚未完成初始化。解决方法先确认my_init()一定返回 0并检查数据/配置实例是否已定义staticintmy_init(conststructdevice*dev){structmy_data*datadev-data;if(dataNULL){return-EINVAL;}printk(Driver Init\n);return0;}同时确认my_data和my_cfg在文件作用域已定义而不是定义在函数内部。3. API 未绑定导致链接失败典型报错undefined reference to my_driver_set原因分析头文件my_driver.h中声明了my_driver_set()和my_driver_get()但my_driver.c中只实现了static的my_set()/my_get()没有提供对外接口或者DEVICE_DEFINE的最后一个参数没有传入api导致 API 结构体没有绑定到设备。解决方法在my_driver.c中补充对外接口并确保DEVICE_DEFINE传入apiintmy_driver_set(conststructdevice*dev,intvalue){conststructmy_driver_api*apidev-api;returnapi-set(dev,value);}intmy_driver_get(conststructdevice*dev,int*value){conststructmy_driver_api*apidev-api;returnapi-get(dev,value);}4. 头文件未包含导致类型未定义典型报错error: unknowntypenamestruct device原因分析my_driver.h中使用了struct device但没有包含zephyr/device.h或者main.c中调用了驱动 API却没有包含my_driver.h。解决方法在my_driver.h顶部补全头文件包含#ifndefMY_DRIVER_H#defineMY_DRIVER_H#includezephyr/device.h#includezephyr/kernel.h/* 驱动 API 结构体 */structmy_driver_api{int(*set)(conststructdevice*dev,intvalue);int(*get)(conststructdevice*dev,int*value);};/* 提供给 Application 调用的接口 */intmy_driver_set(conststructdevice*dev,intvalue);intmy_driver_get(conststructdevice*dev,int*value);#endif/* MY_DRIVER_H */同时在main.c中记得#include my_driver.h。5. 设备名冲突导致重复定义典型报错error: redefinition ofmy_data原因分析my_driver.c中定义了静态实例static struct my_data my_data;但main.c或其他文件中又定义了同名变量或者多个驱动文件使用了相同的设备名mydev导致DEVICE_DEFINE生成的符号冲突。解决方法为数据实例和设备名使用更具辨识度的命名避免与全局符号冲突/* 静态实例使用带前缀的命名 */staticstructmy_datamy_driver_data;staticconststructmy_configmy_driver_cfg{.base0x40000000,.irq5,};/* 注册驱动设备名也加上前缀 */DEVICE_DEFINE(my_driver_dev,MY_DEVICE,my_init,NULL,my_driver_data,my_driver_cfg,POST_KERNEL,50,api);对应地main.c中获取设备时也要同步修改devDEVICE_GET(my_driver_dev);6. 初始化级别或优先级设置不当典型报错warning: device mydev is not ready原因分析DEVICE_DEFINE的初始化级别PRE_KERNEL_1、PRE_KERNEL_2、POST_KERNEL、APPLICATION和优先级数字越小越先执行设置不当导致依赖该设备的其他模块先于它初始化访问时设备尚未就绪。解决方法如果驱动不依赖其他外设使用POST_KERNEL级别即可如果依赖 GPIO、UART 等外设应使用APPLICATION级别确保外设先完成初始化DEVICE_DEFINE(my_driver_dev,MY_DEVICE,my_init,NULL,my_driver_data,my_driver_cfg,APPLICATION,90,api);这样你会完整体验Application ↓ API ↓ Driver ↓ Device ↓ Kernel十六、总结与课后练习恭喜你完成了本课的学习下面用 5 条要点回顾本课的核心知识点Driver FrameworkZephyr 通过统一的设备模型把 Application、Driver API、Driver、硬件分层解耦让应用无需关心具体硬件细节实现硬件抽象与可移植性。Device Model每个硬件外设UART、GPIO、SPI 等在 Zephyr 中都是一个struct device对象保存了 Driver API、Data、Config、Init Function、Name、State 等信息系统启动时自动初始化。DEVICE_DEFINE()这是注册驱动最核心的宏共 9 个参数设备名、名称字符串、初始化函数、PM 回调、数据指针、配置指针、初始化级别、优先级、API 指针负责创建设备对象并加入设备链表。DeviceTree 关联真实硬件设备通常通过DEVICE_DT_DEFINE()与 Devicetree 节点绑定由节点自动生成设备实例纯软件设备则用DEVICE_DEFINE()直接定义。API 绑定驱动通过struct my_driver_api定义自己的操作接口并在DEVICE_DEFINE的最后一个参数传入apiApplication 再通过dev-api调用实现驱动与应用的解耦。课后练习为了巩固所学请完成下面 3 道练习题练习 1将 my_driver 改为使用 DEVICE_DT_DEFINE 注册在 Devicetree 中为设备添加一个节点例如mydev: mydev0 { compatible my,driver; };并把my_driver.c中的注册方式从DEVICE_DEFINE改为DEVICE_DT_DEFINE同时把main.c中的DEVICE_GET(mydev)改为DEVICE_DT_GET(DT_NODELABEL(mydev))。提示DEVICE_DT_DEFINE的第一个参数是 Devicetree 节点标识符而不是自定义的设备名。练习 2为驱动增加一个 reset API在my_driver.h的struct my_driver_api中增加一个int (*reset)(const struct device *dev);成员在my_driver.c中实现static int my_reset(const struct device *dev)把status清零、count归零并把它绑定到api结构体。最后在main.c中调用my_driver_reset(dev)并打印复位后的值。练习 3将初始化级别改为 APPLICATION 并说明原因把DEVICE_DEFINE或DEVICE_DT_DEFINE中的初始化级别从POST_KERNEL改为APPLICATION优先级改为90。思考并说明为什么当驱动依赖 GPIO、UART 等外设时使用APPLICATION级别更安全提示APPLICATION级别在所有内核服务和外设初始化完成之后才执行可以避免设备尚未就绪就被访问的问题。完成以上练习后你就已经掌握了 Zephyr 驱动开发的核心流程从定义 API、实现驱动、注册设备到在 Application 中获取设备并调用接口。接下来可以尝试为真实外设如传感器、LED编写驱动进一步加深理解。
返回列表