u-boot器件驱动模型(Device&Drivers)之链接器的秘密

本文深入探讨了U-Boot中的设备驱动模型,重点分析了宏U_BOOT_DRIVER和U_BOOT_DEVICE的作用及其如何组织驱动程序。通过代码示例,详细解释了链接器在驱动程序加载过程中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

       哈哈,写东西前总喜欢先扯蛋,赶时间的直接无视这段吧。前段时间照着x_project成功的将手上的一块基于nuc972的板子成功移植上了最新的u-boot,相关细节可以参考我的这篇博客

       那篇博客的最后我给自己设立了几个后续要完善的功能,是选择继续模仿着搬砖呢,还是先把一些东西看明白些呢,显然我开始写这篇文章的时候是选择了后者,哈哈。

“慢下来,享受技术” —— 蜗窝科技。

依葫芦画瓢后的疑问

一直以来我大多是处于一种依葫芦画瓢的搬砖状态,同样的下面这部分code展示了我画的瓢,nuc972在u-boot上的串口驱动。

#include <common.h>
#include <asm/io.h>
#include <dm.h>
#include <serial.h>

#include "serial_nuc970.h"

static int nuc970_serial_setbrg(struct udevice *dev, int baudrate)
{
	......
}

static int nuc970_serial_getc(struct udevice *dev)
{
	......
}

static int nuc970_serial_putc(struct udevice *dev, const char c)
{
	......
}

static int nuc970_serial_pending(struct udevice *dev, bool input)
{
	......
}

static const struct dm_serial_ops nuc970_serial_ops = {
	.putc = nuc970_serial_putc,
	.pending = nuc970_serial_pending,
	.getc = nuc970_serial_getc,
	.setbrg = nuc970_serial_setbrg,
};

static int nuc970_serial_probe(struct udevice *dev)
{
	......
}

U_BOOT_DRIVER(nuc970_serial) = {
	.name = "nuc970_serial",
	.id = UCLASS_SERIAL,
	.ops = &nuc970_serial_ops,
	.probe = nuc970_serial_probe,
	.flags = DM_FLAG_PRE_RELOC,
};

U_BOOT_DEVICE(nuc970_serial) = { 
    .name = "nuc970_serial", 
};

为什么code里写的是nuc970,不是说好了是nuc972的吗,因为我是从公版bsp里面沿用下来的,不用纠结,哈哈

那问题来了,device是怎么遇上driver的呢?这个问题似乎是终极问题,要回答它要先弄明白很多东西,那这篇文章我们先来看看宏U_BOOT_DRIVERU_BOOT_DEVICE多做了些什么事,它们是怎么被用起来的。

一切问题的答案多会在code中找到,下面开始代码走起,哈哈

链接器为我们做的那些事

1、脱下宏的外衣

U_BOOT_DRIVERU_BOOT_DEVICE展开得到如下:

struct driver _u_boot_list_2_driver_2_nuc970_serial __aligned(4) \
__attribute__((unused,	section(".u_boot_list_2_driver_2_nuc970_serial"))) = {
	.name = "nuc970_serial",
	.id = UCLASS_SERIAL,
	.ops = &nuc970_serial_ops,
	.of_match = nuc970_serial_ids,
	.probe = nuc970_serial_probe,
	.flags = DM_FLAG_PRE_RELOC,
};

struct driver_info _u_boot_list_2_driver_info_2_nuc970_serial __aligned(4) \
__attribute__((unused,	section(".u_boot_list_2_driver_info_2_nuc970_serial"))) = { 
    .name = "nuc970_serial", 
};

从上面我们可以看到声明他们的时候对它们做了如下要求:

  • 要求它们存放的时候4字节对齐,这通常是为了更方便的访问处理它们;
  • 要求它们存放在一个各自独有的段里面。

那问题又来了,要求他们存放在各自的段里面那肯定是要在链接脚本里体现的啊,让我们赶紧来看下链接脚本。

让我们打开./arch/arm/cpu/u-boot.lds文件,仔细的看我们会看到一段这样的描述:

.............................

     . = ALIGN(4);
     .u_boot_list : {
         KEEP(*(SORT(.u_boot_list*)));
     }
	 
.............................

没错所有以.u_boot_list开头的段多将在这里存放,还有两个奇怪的符号是要做啥呢,赶紧查查,这里是关键的地方千万不要放过。

       google一番后了解到KEEP关键字是为了保证所有的段多被加进来,不要被链接器自作聪明的把某些它认为没有的段舍弃,事实上我们确实是定义了一些会让它看起来没用的段,这个我们后面会提到;而SORT关键字如其名字就是根据段名字串进行排序然后存放,它将起到关键作用,为了配合它我们把段名作了些手脚,你看前面的段名展开后这么长,哈哈

       通过前面的窥探,我们大概知道了用宏U_BOOT_DRIVERU_BOOT_DEVICE声明的变量将被分配到自己一个特有的段下,在链接的时候被组织到一起,那它们是怎么被用起来的呢,还是回到代码里来看看

2、回到起点

让从打开器件驱动模型后执行的第一个函数initf_dm()开始一路往下看去,一路展开如下:

initf_dm()
{
	dm_init_and_scan()
	{
		dm_init()
		{
			device_bind_by_name(&root_info)
			{
				lists_driver_lookup_name("root_driver")
				{
					struct driver *drv = ll_entry_start(struct driver, driver);
					
					const int n_ents = ll_entry_count(struct driver, driver);
					
					for (entry = drv; entry != drv + n_ents; entry++) 
					{
						if (!strcmp(name, entry->name))
							return entry;
					}
				}
			}
		}
	}
}

这里让我们先把注意力放到这句话上struct driver *drv = ll_entry_start(struct driver, driver);可以看到它又是个宏定义,同样来自于头文件Linker_lists.h让我们把它展开来一探究竟,如下:

struct driver *drv = ({	

	static char start[0] __aligned(4) __attribute__((unused, section(".u_boot_list_2_driver_1")));

	(driver *)&start;	
});

static char start[0]是什么鬼,0个元素的字符数组,那就是不占用空间咯,还把它强加到了.u_boot_list_2_driver_1命名的段上,原来它就是那个会让编译器看起来没有用的段,而在链接脚本里给它强加上KEEP关键字,我们再结合这个段名和(driver *)&start;我想你应该能明白了是怎么一回事了吧,哈哈

有start那会不会有end的呢?让我们在Linker_lists.h找下,果然有一宏叫ll_entry_end那就让我们假设有这样一句代码来展开试试,如下:

struct driver *end_drv = ll_entry_start(struct driver, driver);

struct driver *end_drv = ({	

	static char end[0] __aligned(4) __attribute__((unused, section(".u_boot_list_2_driver_3")));

	(driver *)&end;
});

让我们来关注下它的段名.u_boot_list_2_driver_3取的多么的优雅,结合前面我们定义的nuc972的串口驱动,然后再结合链接脚本里SORT关键字的那么一处理,我想你大概明白它们是怎么被组织到一块的了吧,让我们来列列大概如下:

................................
	.u_boot_list_2_driver_1
	.u_boot_list_2_driver_2_nuc970_serial
	.u_boot_list_2_driver_3
................................

哈哈,列好队了,随时待命。可以看出所有driver的结构体变量多被组织到这里,而因为段u_boot_list_2_driver_1u_boot_list_2_driver_3的存在会给我提供一个找到它们的一个起始地址和结束地址。

小结

总结要简单,总结要简单,总结要简单,那在这篇文章里我们总结一句话,如下:

使用Linker_lists.h文件提供的宏声明的相同类型的变量将被链接器安排到一起并被排序,同时它还提供了让我们找到它们的方法

哈哈,怎么变成分析Linker_lists.h文件的作用了,说好的分析器件驱动模型的呢,不急后面我们慢慢看下去,这里先把它搞清楚;其实这些内容在Linker_lists.h已经注释的非常清楚了,点赞,而这里是从code来一步步看清它的。

<think>好的,我现在需要帮助用户在RK3588平台上开发和测试PWM蜂鸣器驱动。根据用户提供的引用内容,特别是引用[1]提到的ITOP-RK3588开发板的PWM接口,只有PWM3、7、11、15支持输入捕获,而PWM15默认用于蜂鸣器。用户想了解如何在这个平台上实现和测试驱动。首先,我需要确认硬件连接。根据引用[1],PWM15连接蜂鸣器,所以用户需要检查硬件原理图,确保硬件连接正确。然后,驱动开发方面,可能需要配置设备树,启用PWM通道。引用[1]提到PWM15用于蜂鸣器,所以设备树需要正确配置PWM15节点。接下来,内核配置部分,用户需要在内核中启用PWM驱动和相关子系统。这可能涉及到makemenuconfig中的选项,比如DeviceDrivers ->PWM Support。另外,驱动代码的实现可能需要使用PWM API,如pwm_request、pwm_config等,如引用[3]提到的驱动开发流程。测试部分,用户可能需要编写应用程序或使用sysfs接口来控制PWM,比如设置占空比和频率。引用[4]提到压电蜂鸣器的工作原理,可能需要调整PWM的频率在可听范围内,通常在2kHz-5kHz之间。同时,使用示波器验证波形是否正确,如引用[4]提到的信号验证。调试过程中可能会遇到时钟冲突问题,需要检查PWM控制器的时钟源配置,确保没有其他设备占用同一时钟。此外,音量调节可能通过改变占空比来实现,但压电蜂鸣器可能对占空比变化不敏感,所以可能需要调整频率。 用户的问题还包括如何动态配置PWM频率,这可以通过设备树传递参数或在驱动中通过ioctl实现。阻塞和非阻塞IO的实现差异可能需要考虑驱动中的等待队列和信号处理。输入子系统的应用可能涉及将蜂鸣器作为输入设备响应事件,如按键触发。总结下来,步骤应该是硬件确认、设备树配置、内核驱动开发、编译测试、调试优化。需要确保每一步都参考了RK3588的具体文档和硬件资料,避免错误。</think>### RK3588 PWM蜂鸣器驱动开发与测试方法 #### 一、硬件准备与验证 1. **确认硬件连接** - 根据ITOP-RK3588开发板原理图,PWM15通道默认连接蜂鸣器[^1] - 检查开发板蜂鸣器电路是否为PWM驱动型(压电式蜂鸣器需特定频率驱动)[^4] - 使用万用表测量蜂鸣器两端电压,确认供电正常(通常为3.3V或5V) 2. **硬件接口特性** - 压电式蜂鸣器工作频率范围:$f_{min}=2\text{kHz}$ ~ $f_{max}=5\text{kHz}$[^4] - 驱动电压幅值需满足:$V_{pp} \geq 3V$(具体参考器件手册) #### 二、驱动开发流程 1. **设备树配置** 在`arch/arm64/boot/dts/rockchip/rk3588s.dtsi`中添加PWM节点配置: ```dts &pwm15 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&pwm15m0_pins>; // 定义默认频率与占空比 buzzer { compatible = "pwm-beeper"; pwms = <&pwm15 0 1000000>; // 周期1ms(频率1kHz) duty-cycle = <500000>; // 占空比50% }; }; ``` 编译设备树:`make dtbs`[^1] 2. **内核配置** 启用PWM子系统与蜂鸣器驱动: ```bash make menuconfig ``` 路径: ``` Device Drivers → Pulse-Width Modulation (PWM) Support → <*> Rockchip PWM support <*> PWM buzzer support ``` 3. **驱动代码实现** 创建`drivers/input/misc/pwm-beeper.c`,核心操作函数: ```c static int pwm_beeper_probe(struct platform_device *pdev) { struct pwm_device *pwm = devm_pwm_get(&pdev->dev, NULL); pwm_config(pwm, 500000, 1000000); // 占空比50% pwm_enable(pwm); return 0; } static const struct of_device_id pwm_beeper_match[] = { { .compatible = "pwm-beeper" }, { } }; ``` #### 三、功能测试方法 1. **sysfs直接控制** 加载驱动后,通过文件系统接口调试: ```bash # 查看PWM设备 ls /sys/class/pwm/pwmchip15/ # 激活PWM通道 echo 0 > /sys/class/pwm/pwmchip15/export # 设置周期(单位:ns) echo 1000000 > pwm0/period # 1kHz频率 echo 500000 > pwm0/duty_cycle # 50%占空比 echo 1 > pwm0/enable ``` 2. **用户空间测试程序** 编写C程序通过ioctl控制: ```c #include <fcntl.h> #include <linux/pwm.h> int main() { int fd = open("/dev/pwm15", O_RDWR); struct pwm_state state = { .period = 1000000, // 1ms周期 .duty_cycle = 300000,// 30%占空比 .enabled = 1 }; ioctl(fd, PWM_IOC_SET_STATE, &state); close(fd); return 0; } ``` #### 四、调试技巧 1. **信号完整性验证** - 使用示波器测量PWM15引脚波形,确认频率满足: $$ f = \frac{1}{T} = \frac{1}{1 \times 10^{-3}} = 1\text{kHz} $$ - 检查占空比误差:$\Delta D = |D_{实际} - D_{设定}| \leq 2\%$ 2. **常见问题排查** - **无输出**:检查时钟树配置,确认PWM控制器时钟源已使能 - **频率偏差**:调整`pwm-clock`参数,确保时钟分频正确 - **蜂鸣器异响**:尝试修改PWM频率至$[2.5k, 4k]\text{Hz}$范围内[^4] ####
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值