ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OpenHarmony HDF驱动AD9833:SPI与IIO集成实战

OpenHarmony HDF驱动AD9833:SPI与IIO集成实战 1. 项目缘起与整体设计思路1.1 为什么要在 OpenHarmony 上折腾 AD9833先说说这个项目的来龙去脉。我手头有一块瑞芯微 RK3568 的开发板跑的是 OpenHarmony 轻量级到小型系统的适配版本平时主要用来做工业信号采集和波形输出的验证。AD9833 这颗芯片相信搞硬件的朋友都不陌生——ADI 出品的一款低功耗 DDS 波形发生器能输出正弦波、三角波和方波频率最高到 12.5MHzSPI 接口控制价格便宜外围电路简单非常适合做信号源、扫频仪、阻抗测量这类应用。问题在于OpenHarmony 的 HDF 驱动框架和传统的 Linux 字符设备驱动写法差别不小尤其是涉及 SPI 子系统、设备树配置、IIO 子系统对接这几个环节网上能找到的完整案例少之又少。我翻了不少资料大部分要么是 STM32 裸机驱动 AD9833要么是 Linux 下用 spidev 直接操作真正在 OpenHarmony 上把 AD9833 跑通并且接入 IIO 框架的几乎没有现成的参考。所以这个项目的核心目标很明确在 OpenHarmony 系统上通过标准 SPI 总线驱动 AD9833 模块实现频率和波形的动态配置并且尽可能对接 IIO 子系统让上层应用能像读传感器一样读取和设置波形参数。适合谁看如果你正在做 OpenHarmony 的外设驱动开发或者手头有 RK3568、RK3588 这类平台想搞清楚设备树怎么配、SPI 怎么调、HDF 驱动怎么写这篇内容应该能帮你省下不少查资料的时间。1.2 整体方案选型与架构拆解整个方案我分成了四层来看从下往上依次是硬件层、内核设备树层、HDF 驱动层、应用接口层。硬件层就是 AD9833 模块本身加上 SPI 总线的物理连接。AD9833 的 SPI 接口比较特殊它是三线制 SPI没有 MISO只有 SCLK、SDATAMOSI和 FSYNC片选时钟最高 40MHz但实际用下来 10MHz 左右比较稳。模块上一般还会带一个运放做输出缓冲有的还带低通滤波器这些外围不影响驱动逻辑。内核设备树层是第一个关键点。RK3568 的 SPI 控制器在设备树里需要正确配置引脚复用、时钟、DMA 通道还要在 SPI 节点下挂载 AD9833 作为子节点。这里有个坑OpenHarmony 用的内核虽然基于 Linux但设备树的解析和 HDF 的匹配机制有自己的规则不能完全照搬 Linux 的写法。HDF 驱动层是核心。OpenHarmony 的 HDF 框架提供了 SPI 接口的抽象包括SpiOpen、SpiTransfer这些 API但和 Linux 的 spidev 不一样HDF 的 SPI 操作需要先通过DeviceHandle拿到控制器句柄再构造SpiMsg进行传输。AD9833 的寄存器是 16 位的每次写两个字节这个在 HDF 里需要处理好字节序和片选时序。应用接口层我选择对接 IIO 子系统。IIO 是 Linux 内核里专门给 ADC、DAC、传感器这类设备用的框架OpenHarmony 也支持。把 AD9833 注册成 IIO 设备的好处是上层可以通过标准接口读写频率、波形类型这些参数不用自己再封装一套 ioctl。虽然 AD9833 严格来说不是标准的 IIO 设备但它的输出频率、相位、波形类型这些属性用 IIO 的 channel 和 attribute 来表达很自然。注意OpenHarmony 的 IIO 支持和标准 Linux 有差异部分 attribute 的注册方式需要适配 HDF 的驱动模型不能直接套用 Linux 的 iio_info 结构体。1.3 关键参数与性能预期在动手之前先把几个关键指标定下来后面调试的时候有参照。参数项目标值说明SPI 时钟频率10MHzAD9833 最高 40MHz但 RK3568 SPI 控制器在 10MHz 下时序最稳输出频率范围0.1Hz - 1MHz受限于模块上的输出滤波器和运放带宽频率分辨率0.1HzAD9833 是 28 位频率寄存器主时钟 25MHz 时分辨率约 0.093Hz波形类型正弦、三角、方波通过控制寄存器切换更新速率100Hz即每秒最多更新 100 次频率受 SPI 传输和 IIO 接口开销限制相位调节0-360度12位分辨率两个相位寄存器可选这些参数不是拍脑袋定的SPI 时钟 10MHz 是实测下来的稳定值再高会出现偶发数据错误。输出频率上限 1MHz 是因为模块上的低通滤波器截止频率大概在 1.5MHz 左右再高波形失真明显。更新速率 100Hz 是考虑到每次频率更新需要写两个 16 位寄存器加上 IIO 的 sysfs 接口开销实际测下来 100Hz 左右比较合理。2. 核心细节解析与实操要点2.1 AD9833 寄存器模型与 SPI 时序要点AD9833 的寄存器结构不复杂但有几个细节容易踩坑。它内部有五个寄存器控制寄存器16位、频率寄存器028位分两次写、频率寄存器128位、相位寄存器012位、相位寄存器112位。控制寄存器是关键它的位定义如下D15、D14必须为 0否则芯片不响应D13B2828位频率寄存器写入模式。置1时连续写两次16位数据组成28位频率值D12HLB28位写入时的高低位选择。B281时第一次写忽略HLB第二次写时HLB0表示写低14位HLB1表示写高14位D11FSELECT频率寄存器选择。0选FREQ01选FREQ1D10PSELECT相位寄存器选择D9保留必须为0D8RESET复位位。置1时芯片复位内部寄存器清零D7SLEEP12DAC 电源控制D6SLEEP1内部时钟控制D5OPBITEN方波输出使能D4保留D3DIV2方波分频控制D2MODE波形模式。0正弦1三角D1、D0保留SPI 时序方面AD9833 的 SDATA 在 SCLK 下降沿采样FSYNC 低电平有效。每次传输 16 位MSB 先出。FSYNC 拉低后需要等待至少 10ns 才能开始 SCLK传输完成后 FSYNC 拉高再等待至少 10ns 才能开始下一次传输。这个时序在 HDF 的 SPI 传输里需要特别注意因为 HDF 的SpiTransfer默认会连续发送多个消息如果片选控制不当会出现数据错位。实操心得我在第一次调试时FSYNC 用的是软件片选结果发现每次传输后片选拉高的时机比预期晚了几个时钟周期导致 AD9833 把下一个字节的前几位也吃进去了。后来改成硬件片选由 SPI 控制器自动控制 FSYNC问题才解决。RK3568 的 SPI 控制器支持硬件片选在设备树里把cs-gpios配好就行。2.2 OpenHarmony HDF SPI 接口的使用方式HDF 的 SPI 接口和 Linux 的 spidev 差别挺大这里详细说一下。首先需要在驱动入口的Bind函数里拿到 SPI 控制器的DeviceHandle然后调用SpiOpen打开设备。关键 API 如下DevHandle SpiOpen(struct HdfDeviceObject *device); void SpiClose(DevHandle handle); int32_t SpiTransfer(DevHandle handle, struct SpiMsg *msgs, uint32_t count);SpiMsg结构体里需要填wbuf、rbuf、len、keepCs、delayUs这几个字段。对于 AD9833因为只写不读rbuf可以设为 NULLlen设为 2两个字节keepCs设为 0 表示每次传输后释放片选delayUs设为 10 微秒保证时序间隔。这里有个容易忽略的点HDF 的 SPI 传输默认是按字节流处理的但 AD9833 需要的是 16 位数据MSB 先出。所以在填充wbuf时需要把 16 位数据拆成两个字节高字节在前。比如要写控制寄存器 0x2000wbuf[0] 0x20wbuf[1] 0x00。另外HDF 的 SPI 控制器驱动在 RK3568 上默认可能没有使能 DMA对于 AD9833 这种小数据量传输用 PIO 模式就够了没必要折腾 DMA。但如果你的项目里 SPI 总线上还挂了其他大数据量的设备比如 SPI Flash 或者显示屏那就需要考虑 DMA 通道的分配问题。2.3 设备树配置的关键节点RK3568 的设备树配置是绕不开的一步。首先要在 SPI 控制器节点下添加 AD9833 子节点。以 SPI1 为例设备树大概长这样spi1 { status okay; pinctrl-names default; pinctrl-0 spi1m0_cs0 spi1m0_pins; num-cs 1; cs-gpios gpio3 RK_PA1 GPIO_ACTIVE_LOW; ad9833: ad98330 { compatible adi,ad9833; reg 0; spi-max-frequency 10000000; spi-cpol; spi-cpha; status okay; }; };几个关键点解释一下。spi-max-frequency设为 10MHz和前面定的目标一致。spi-cpol和spi-cpha这两个属性决定了 SPI 的时钟极性和相位AD9833 需要 CPOL1、CPHA1也就是空闲时 SCLK 为高数据在第二个边沿采样。这个配置如果搞错了数据完全写不进去而且现象很隐蔽芯片不报错就是没输出。cs-gpios指定了硬件片选引脚这里用的是 GPIO3_A1。RK3568 的 GPIO 编号计算方式是bank * 32 group * 8 indexGPIO3_A1 就是 332 08 1 97。在设备树里用RK_PA1这种宏更直观。还有一个坑OpenHarmony 的设备树解析和标准 Linux 有细微差别compatible属性的匹配规则在 HDF 里是通过HdfDeviceObject的property来读取的需要在驱动入口的Init函数里用DevicePropertyReadString读取compatible值然后和驱动里注册的compatible做匹配。如果匹配不上驱动根本不会加载。注意事项修改设备树后需要重新编译内核并更新 boot 分区。RK3568 的 boot 分区里包含内核镜像和设备树 dtb更新时注意不要覆盖其他分区。我一般用rkdeveloptool工具单独烧写 boot 分区比全量烧写快很多。2.4 IIO 子系统对接的适配思路把 AD9833 注册成 IIO 设备主要是为了上层能通过标准接口访问。在 Linux 里IIO 设备会在/sys/bus/iio/devices/iio:deviceX/下生成一堆 attribute 文件比如out_altvoltage0_frequency、out_altvoltage0_phase这些。OpenHarmony 的 IIO 框架基本沿用了 Linux 的这套但在 HDF 驱动里注册 IIO 设备的方式不太一样。我的做法是在 HDF 驱动的Init阶段先完成 SPI 控制器的初始化和 AD9833 的复位然后调用IioDeviceCreate创建一个 IIO 设备对象再通过IioDeviceAddChannel添加 channel。每个 channel 对应一个可读写的属性比如频率 channel、相位 channel、波形类型 channel。这里有个取舍AD9833 的波形类型正弦、三角、方波在标准 IIO 里没有直接对应的 attribute我最后是用out_altvoltage0_wavetype这个自定义 attribute 来实现的。虽然不太标准但能用上层应用通过cat和echo就能切换波形。IIO 的读写回调函数需要自己实现在回调里把上层传来的字符串转成频率值再通过 SPI 写到 AD9833。频率值的计算方式是FREQREG (fout * 2^28) / fclk其中 fclk 是 AD9833 的主时钟一般是 25MHz。比如要输出 1kHzFREQREG (1000 * 268435456) / 25000000 10737.4取整就是 10737。3. 实操过程与核心环节实现3.1 硬件连接与上电检查先把手头的硬件理清楚。我用的 AD9833 模块是市面上常见的那种板载 25MHz 晶振输出经过一个运放缓冲带一个简单的 RC 低通滤波器。模块引脚如下模块引脚连接目标说明VCC3.3V模块供电注意不要接 5VGNDGND共地SCLKSPI1_CLKSPI 时钟SDATASPI1_MOSI数据输入FSYNCSPI1_CS0片选低有效OUT示波器/负载波形输出接线的时候注意 SPI 时钟线不要太长我一开始用杜邦线接了 20cm结果 10MHz 下波形严重畸变后来换成 10cm 的短线就正常了。如果实在需要长线可以考虑降低 SPI 时钟到 5MHz或者加一个缓冲器。上电后先别急着写驱动用示波器看一下 SCLK 和 FSYNC 有没有信号。如果 SCLK 一直为高或者一直为低说明 SPI 控制器没配置好先回去检查设备树。FSYNC 在上电后应该保持高电平只有在传输时才拉低。3.2 设备树修改与内核编译设备树的修改前面已经说了这里补充一下编译和烧写的具体步骤。RK3568 的 OpenHarmony 源码里设备树文件一般在kernel/linux/linux-5.10/arch/arm64/boot/dts/rockchip/目录下找到你板子对应的 dts 文件比如rk3568-evb1-ddr4-v10.dts在里面添加 AD9833 节点。编译内核的命令是./build.sh --product-name rk3568 --build-target kernel编译完成后生成的 boot.img 在out/rk3568/packages/phone/images/目录下。烧写用rkdeveloptoolrkdeveloptool db rk356x_spl_loader.bin rkdeveloptool wl 0x00008000 boot.img rkdeveloptool rd烧写完成后重启进入系统后先检查 SPI 控制器有没有注册成功ls /sys/bus/spi/devices/如果看到spi1.0这样的目录说明设备树里的 SPI 子节点已经被识别了。如果没有检查dmesg里有没有 SPI 相关的报错。3.3 HDF 驱动框架搭建与 SPI 初始化HDF 驱动的代码结构分三部分驱动入口、驱动服务、驱动设备。入口部分负责注册驱动和匹配设备服务部分提供对外接口设备部分负责具体的硬件操作。先看驱动入口static int32_t Ad9833DriverBind(struct HdfDeviceObject *device) { struct Ad9833Driver *drv (struct Ad9833Driver *)OsalMemCalloc(sizeof(*drv)); if (drv NULL) { return HDF_ERR_MALLOC_FAIL; } drv-device device; device-service drv-service; return HDF_SUCCESS; } static int32_t Ad9833DriverInit(struct HdfDeviceObject *device) { struct Ad9833Driver *drv (struct Ad9833Driver *)device-service; if (drv NULL) { return HDF_ERR_INVALID_OBJECT; } return Ad9833SpiInit(drv); }Ad9833SpiInit函数里做几件事打开 SPI 设备、复位 AD9833、配置初始频率和波形。打开 SPI 设备的代码static int32_t Ad9833SpiInit(struct Ad9833Driver *drv) { drv-spiHandle SpiOpen(drv-device); if (drv-spiHandle NULL) { HDF_LOGE(SpiOpen failed); return HDF_FAILURE; } Ad9833Reset(drv); Ad9833SetFrequency(drv, 1000); Ad9833SetWaveform(drv, WAVE_SINE); return HDF_SUCCESS; }复位操作就是往控制寄存器写 0x2100RESET1B281然后写 0x2000RESET0B281退出复位。3.4 频率与波形配置的完整实现频率配置是核心功能代码逻辑如下static int32_t Ad9833SetFrequency(struct Ad9833Driver *drv, uint32_t freq) { uint32_t freqReg; uint16_t ctrlReg; uint8_t wbuf[2]; freqReg (uint32_t)(((uint64_t)freq * 268435456ULL) / 25000000ULL); freqReg 0x0FFFFFFF; ctrlReg 0x2000; wbuf[0] (ctrlReg 8) 0xFF; wbuf[1] ctrlReg 0xFF; Ad9833SpiWrite(drv, wbuf, 2); wbuf[0] (freqReg 8) 0xFF; wbuf[1] freqReg 0xFF; Ad9833SpiWrite(drv, wbuf, 2); wbuf[0] ((freqReg 14) 8) 0xFF; wbuf[1] (freqReg 14) 0xFF; Ad9833SpiWrite(drv, wbuf, 2); return HDF_SUCCESS; }这里注意频率寄存器的写入顺序先写低 14 位再写高 14 位。因为 B281 时第一次写的是低 14 位第二次写的是高 14 位。代码里freqReg 14就是取高 14 位。波形切换的代码static int32_t Ad9833SetWaveform(struct Ad9833Driver *drv, uint8_t wave) { uint16_t ctrlReg 0x2000; uint8_t wbuf[2]; switch (wave) { case WAVE_SINE: ctrlReg | 0x0000; break; case WAVE_TRIANGLE: ctrlReg | 0x0002; break; case WAVE_SQUARE: ctrlReg | 0x0028; break; default: return HDF_ERR_INVALID_PARAM; } wbuf[0] (ctrlReg 8) 0xFF; wbuf[1] ctrlReg 0xFF; Ad9833SpiWrite(drv, wbuf, 2); return HDF_SUCCESS; }方波的配置稍微特殊一点需要同时置位 OPBITEN 和 DIV2这样输出的是方波而不是正弦波。3.5 IIO 设备注册与上层接口验证IIO 设备的注册在Ad9833DriverInit的最后一步完成static int32_t Ad9833IioInit(struct Ad9833Driver *drv) { drv-iioDev IioDeviceCreate(ad9833, drv); if (drv-iioDev NULL) { return HDF_FAILURE; } IioDeviceAddChannel(drv-iioDev, out_altvoltage0_frequency, Ad9833ReadFreq, Ad9833WriteFreq, drv); IioDeviceAddChannel(drv-iioDev, out_altvoltage0_wavetype, Ad9833ReadWave, Ad9833WriteWave, drv); return HDF_SUCCESS; }注册完成后在系统里应该能看到/sys/bus/iio/devices/iio:device0/目录。验证方法echo 5000 /sys/bus/iio/devices/iio:device0/out_altvoltage0_frequency cat /sys/bus/iio/devices/iio:device0/out_altvoltage0_frequency如果输出是 5000说明频率设置成功。再用示波器看输出应该能看到 5kHz 的正弦波。4. 常见问题与排查技巧实录4.1 SPI 通信不生效的排查思路SPI 通信不生效是最常见的问题现象是驱动加载了但 AD9833 没输出。排查步骤我整理了一个速查表现象可能原因排查方法无输出SCLK 无信号SPI 控制器未使能检查设备树 status 是否为 okaySCLK 有信号FSYNC 一直高片选配置错误检查 cs-gpios 和 num-cs有输出但频率不对频率寄存器计算错误核对 fclk 和计算公式输出波形失真SPI 时钟过高降低 spi-max-frequency 到 5MHz输出时有时无时序间隔不足增加 delayUs 到 20 微秒我遇到最诡异的一次是 SCLK 和 FSYNC 都正常但 AD9833 就是没输出。后来用逻辑分析仪抓波形发现 SDATA 上的数据在 SCLK 上升沿就变化了而 AD9833 需要的是下降沿采样。原因是设备树里spi-cpha没配默认 CPHA0改成 CPHA1 后问题解决。4.2 设备树配置的典型错误设备树配置错误往往比较隐蔽因为内核不会报错只是设备不工作。几个典型错误第一个是reg属性写错。SPI 子节点的reg值对应片选编号如果cs-gpios里只有一个片选reg必须是 0。我见过有人写成 1结果驱动一直匹配不上。第二个是引脚复用冲突。RK3568 的 SPI1 引脚和 UART、I2C 有复用关系如果 pinctrl 没配对SPI 信号出不来。检查方法是看/sys/kernel/debug/pinctrl/下的引脚状态。第三个是compatible字符串不匹配。HDF 驱动里注册的compatible必须和设备树里写的完全一致包括大小写和连字符。我一般会在驱动里打印出读取到的compatible值和预期值对比。避坑技巧修改设备树后先用fdtdump工具反编译 dtb 文件确认修改已经生效。命令是fdtdump boot.dtb | grep ad9833如果有输出说明设备树里确实有这个节点。4.3 IIO 接口读写异常的解决IIO 接口读写异常通常表现为echo命令报错或者cat出来是乱码。原因可能有几个一是 attribute 名称不对。IIO 的 attribute 命名有规范out_altvoltage0_frequency这种格式是标准写法如果写成frequency或者freq上层工具可能识别不了。二是回调函数返回值不对。IIO 的读回调需要返回实际读取的字节数写回调需要返回实际写入的字节数。如果返回 0 或者负数上层会认为操作失败。三是并发访问问题。如果多个进程同时读写 IIO attribute可能会出现数据竞争。我的做法是在驱动里加一个互斥锁保证同一时间只有一个操作在进行。4.4 性能优化与稳定性提升跑通之后我做了几轮优化。首先是 SPI 传输的批量处理把频率更新需要的三次写操作合并成一个SpiTransfer调用减少片选切换次数。实测下来更新速率从 60Hz 提升到了 120Hz。其次是 IIO 回调里的字符串转换优化。原来的实现用snprintf和sscanf开销比较大。后来改成自己写简单的整数转字符串和字符串转整数函数CPU 占用降低了大概 15%。稳定性方面我在驱动里加了一个看门狗机制每隔 1 秒检查一次 AD9833 的控制寄存器状态如果发现异常就自动复位并重新配置。这个机制在长时间运行测试中救了我好几次有一次因为电源波动导致 AD9833 内部状态错乱看门狗自动恢复了。最后再分享一个小技巧AD9833 的相位寄存器可以用来做频率微调。虽然它的分辨率只有 12 位但在某些需要精确相位控制的场景下很有用。比如做锁相环或者正交信号生成时通过调节两个相位寄存器的值可以实现 0-360 度的相位偏移。这个功能我在一个阻抗测量项目里用过效果不错。
返回列表