1. 从一个波形发生模块说起:为什么选AD9833和OpenHarmony
搞嵌入式开发的朋友对信号发生这件事应该都不陌生。无论是做传感器激励、音频测试、还是工业现场的载波生成,手头有一个能输出稳定正弦波、三角波、方波的信号源,很多调试工作会轻松不少。AD9833就是在这个需求下被大量使用的经典芯片——ADI出品,低功耗、SPI接口、最高25MHz时钟、能输出三种波形,价格便宜,模块化程度高,几乎成了入门级DDS(直接数字频率合成)方案的代名词。
但这次我们要聊的不是在STM32上跑AD9833,也不是在裸机环境里写寄存器。这次的关键词是OpenHarmony。具体来说,是在OpenHarmony系统环境下,通过标准Linux内核驱动框架(设备树 + SPI子系统 + IIO子系统)来驱动AD9833模块,让一个原本在单片机世界里“写几个寄存器就能跑”的小芯片,融入到完整的操作系统设备模型里。
这件事的价值在哪里?如果你只是做一个小玩具,裸机驱动AD9833确实十分钟就能出波形。但如果你在做的是一个需要多任务调度、网络通信、远程控制、数据采集与可视化并存的系统——比如工业物联网网关、实验室自动化测试平台、智能传感器节点——那么把AD9833纳入OpenHarmony的统一设备管理框架就变得非常必要。你需要它像其他标准设备一样被枚举、被配置、被用户态程序通过标准接口访问,而不是在某个角落里写死一段SPI时序代码。
这篇文章适合谁看?适合已经对OpenHarmony有一定了解、知道怎么编译内核和烧录系统,但对Linux设备驱动模型还不够熟悉的开发者;也适合那些在瑞芯微RK3568、RK3588等平台上做OpenHarmony移植,需要接入自定义SPI外设的工程师。我会从设备树配置讲起,一路讲到IIO子系统的接入和用户态验证,把中间踩过的坑和想明白的道理都摊开来说。
2. 整体设计思路:为什么不能绕过设备树和IIO
2.1 裸机思维与系统思维的差异
很多从STM32转过来的开发者,第一次在OpenHarmony或Linux环境下驱动SPI外设时,最直观的想法是:“我直接操作SPI控制器寄存器不就行了?”或者“我写个字符设备驱动,在里面手动拉CS、发数据,不也一样?”
理论上当然可以。但这样做会带来几个问题。第一,SPI控制器是共享资源,系统里可能同时挂着SPI Flash、显示屏、传感器等多个设备,你手动操作寄存器会破坏总线仲裁和互斥机制。第二,你绕过了内核的电源管理和时钟框架,设备休眠唤醒时你的驱动大概率会挂。第三,用户态程序无法通过标准接口访问你的设备,每换一个应用就要重新写一套ioctl,维护成本极高。
所以正确的做法是:让AD9833成为一个标准的SPI从设备,挂载在SPI总线上,由内核的SPI子系统负责总线通信;同时把它注册为一个IIO设备,让用户态可以通过sysfs或字符设备节点读取和配置波形参数。这样做的代价是前期配置复杂一些,但后期扩展性和可维护性完全不是一个量级。
2.2 设备树:硬件描述与驱动分离的基石
设备树(Device Tree)的核心思想是“硬件描述与驱动代码分离”。同一份AD9833驱动代码,可以在RK3568上跑,也可以在RK3588上跑,甚至换一个SPI控制器也无需修改驱动源码,只需要改设备树里SPI节点的配置。
在OpenHarmony的标准系统中,内核部分通常基于Linux内核(或LiteOS-M/A混合部署),设备树的使用方式与标准Linux一致。你需要做的是:在SPI控制器的设备树节点下,添加一个子节点来描述AD9833,包括它的片选信号、SPI频率、通信模式等参数。内核启动时,SPI子系统会根据设备树中的compatible属性匹配对应的驱动,自动完成设备与驱动的绑定。
2.3 IIO子系统:为什么不用字符设备
IIO(Industrial I/O)子系统是Linux内核专门为ADC、DAC、传感器、频率合成器等模拟器件设计的框架。它提供了统一的sysfs接口和字符设备接口,支持缓冲区、触发、事件等高级功能。
对于AD9833这种DDS芯片,虽然它本身没有ADC采集功能,但它属于“信号生成”类设备,IIO框架下的DAC子类正好适用。通过IIO,用户态可以直接通过/sys/bus/iio/devices/iio:deviceX/下的属性文件来设置频率、波形类型、相位等参数,不需要自己写一套ioctl。而且IIO框架天然支持与其它IIO设备(比如ADC)联动,方便做闭环控制。
注意:AD9833的IIO驱动在标准Linux内核中并不存在,需要自己编写或移植。但设备树和SPI子系统的配置是通用的,这部分工作无论你最终选择哪种用户态接口都无法省略。
3. 核心细节解析:设备树配置与SPI参数计算
3.1 SPI控制器节点确认
在RK3568或RK3588的设备树中,SPI控制器通常以spi0、spi1、spi2等命名。以RK3568为例,常见的SPI控制器节点在arch/arm64/boot/dts/rockchip/rk3568.dtsi中定义,类似如下结构:
spi0: spi@fe610000 { compatible = "rockchip,rk3568-spi", "rockchip,rk3066-spi"; reg = <0x0 0xfe610000 0x0 0x1000>; interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru CLK_SPI0>, <&cru PCLK_SPI0>; clock-names = "spiclk", "apb_pclk"; dmas = <&dmac0 20>, <&dmac0 21>; dma-names = "tx", "rx"; pinctrl-names = "default", "high_speed"; pinctrl-0 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins>; pinctrl-1 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins_hs>; num-cs = <2>; status = "disabled"; };你需要在你的板级设备树文件(通常是rk3568-yourboard.dts)中引用这个节点,并将status改为"okay",同时添加AD9833子节点。
3.2 AD9833子节点配置详解
AD9833的SPI接口有几个关键特性需要体现在设备树中:
- SPI模式:AD9833支持SPI模式2(CPOL=1, CPHA=0)和模式0(CPOL=0, CPHA=0),具体取决于MISO/SDATA引脚的接法。模块上通常默认模式2。
- 最大时钟频率:AD9833的SCLK最高40MHz,但实际使用中建议不超过25MHz,以保证信号完整性。
- 字长:16位。
- 片选:AD9833有独立的FSYNC引脚作为片选,低电平有效。
一个典型的设备树子节点如下:
&spi0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi0m0_cs0 &spi0m0_pins>; num-cs = <1>; ad9833: ad9833@0 { compatible = "adi,ad9833"; reg = <0>; spi-max-frequency = <25000000>; spi-cpol; spi-cpha = <0>; spi-cs-high; spi-3wire; spi-word-delay-us = <1>; }; };这里有几个点需要展开说。
reg = <0>表示使用SPI控制器的第0号片选(CS0)。如果你用的是硬件片选,这个值必须与实际的CS引脚编号对应。如果使用GPIO作为软件片选,则需要额外配置cs-gpios属性。
spi-cpol表示CPOL=1,即时钟空闲时为高电平。spi-cpha = <0>表示CPHA=0,即数据在时钟的第一个边沿采样。这两个参数组合起来就是SPI模式2。如果你发现通信不上,第一个要检查的就是这两个参数是否与AD9833模块的实际接法匹配。
spi-cs-high这个属性容易被忽略。AD9833的FSYNC是低电平有效,所以正常情况下不需要这个属性。但如果你用的是某些电平转换电路导致极性反转,可能就需要加上。我建议先用逻辑分析仪抓一下CS、SCLK、SDATA三根线的波形,确认极性后再决定。
spi-3wire表示三线制SPI,即没有MISO线,只有MOSI。AD9833确实只支持写操作,没有回读功能,所以三线制是正确的。
spi-word-delay-us = <1>是一个比较微妙的参数。它表示每个字之间插入1微秒的延迟。AD9833的时序要求中,FSYNC拉高到下一次拉低之间需要至少10ns的延迟,SCLK最后一个下降沿到FSYNC拉高之间也需要至少10ns。在高速SPI下,这些延迟可能自然满足,但在某些控制器上,CS的建立和保持时间可能不够,加上这个参数可以增加裕量。
3.3 片选信号的硬件与软件选择
SPI片选有两种实现方式:硬件片选和软件片选(GPIO片选)。
硬件片选由SPI控制器自动管理,优点是时序精准、不占用CPU,缺点是片选数量受控制器限制,且引脚位置固定。RK3568的SPI0通常有2个硬件片选,如果你要挂多个SPI设备,可能不够用。
软件片选通过GPIO模拟,优点是灵活、数量不受限,缺点是需要驱动中手动控制,且时序精度取决于GPIO操作速度。在设备树中配置软件片选的方式如下:
&spi0 { cs-gpios = <&gpio1 RK_PA4 GPIO_ACTIVE_LOW>; ... };这里GPIO_ACTIVE_LOW表示低电平有效。配置了cs-gpios后,SPI子系统会在每次传输前自动拉低对应GPIO,传输完成后拉高,不需要驱动代码干预。
实操心得:如果你的AD9833模块上已经有电平转换芯片或缓冲器,片选信号的驱动能力可能不足,导致CS下降沿不够陡峭。这种情况下,建议在CS线上加一个下拉电阻(比如10kΩ),确保空闲时CS稳定在高电平。
3.4 SPI频率与信号完整性的权衡
AD9833的SCLK最高频率是40MHz,但实际能跑多快取决于你的PCB走线、线缆长度和上拉电阻。在RK3568开发板上,SPI0的默认时钟源是PLL,可以分频到很宽的频率范围。
我一般建议从10MHz开始调试,确认通信正常后再逐步提高。如果发现波形输出不稳定或频率偏差大,优先降低SPI频率试试。另外,spi-max-frequency属性设置的是上限值,实际传输频率由驱动根据时钟源分频计算,不一定完全等于设定值。
计算分频的公式大致是:SPI实际频率 = 时钟源频率 / (2 * (div + 1)),其中div是分频系数。以RK3568的SPI0为例,如果时钟源是198MHz,要得到10MHz,div大约为9,实际频率为198 / (2 * 10) = 9.9MHz。这个偏差在AD9833的容忍范围内,不会影响输出频率精度,因为AD9833的输出频率由内部寄存器值决定,与SPI时钟无关。
4. 实操过程:从设备树到波形输出
4.1 设备树修改与内核编译
第一步是在板级设备树文件中添加AD9833节点。假设你的板级文件是rk3568-evb1-ddr4-v10.dts,找到&spi0节点,按上一节的模板添加子节点。
修改完成后,重新编译内核设备树:
./build.sh --product-name rk3568 --build-target kernel如果你使用的是OpenHarmony的编译框架,设备树的编译通常集成在内核编译过程中。编译完成后,生成的dtb文件会打包进boot.img或单独的分区。
烧录后,系统启动时可以通过以下命令确认设备树是否生效:
ls /proc/device-tree/spi@fe610000/如果看到ad9833@0目录,说明设备树节点已被正确解析。
4.2 SPI设备枚举确认
设备树生效后,SPI子系统会自动枚举总线上的设备。通过以下命令查看:
ls /sys/bus/spi/devices/你应该能看到类似spi0.0的设备名。如果看不到,说明设备树配置有问题,或者SPI控制器没有使能。
进一步查看设备详情:
cat /sys/bus/spi/devices/spi0.0/modalias如果输出包含"ad9833",说明设备与驱动的匹配字符串正确。
4.3 IIO设备注册与属性访问
假设你已经编写或移植了AD9833的IIO驱动,加载后会在/sys/bus/iio/devices/下生成iio:deviceX节点。进入该目录,你会看到一系列属性文件:
ls /sys/bus/iio/devices/iio:device0/常见的属性包括:
- out_altvoltage0_frequency:输出频率,单位Hz
- out_altvoltage0_phase:相位偏移
- out_altvoltage0_waveform:波形类型(正弦、三角、方波)
- out_altvoltage0_enable:输出使能
设置频率的示例:
echo 1000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage0_frequency设置波形为正弦波:
echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage0_waveform注意:不同驱动实现下属性名称可能略有差异,具体以你的驱动代码为准。如果属性文件不存在,说明驱动没有正确注册IIO设备,需要检查驱动中的iio_device_register调用。
4.4 用户态验证与波形观测
配置完成后,用示波器或逻辑分析仪接AD9833的VOUT引脚,应该能看到对应频率和波形的信号。如果没有输出,按以下顺序排查:
- 确认AD9833的电源和参考时钟正常。AD9833需要外部提供25MHz晶振或时钟信号,模块上通常已经集成。
- 确认SPI通信正常。用逻辑分析仪抓SCLK、SDATA、FSYNC三根线,看是否有数据发出。
- 确认IIO属性写入成功。读取属性值看是否与写入一致。
- 确认输出使能位已设置。有些驱动默认不使能输出,需要显式写入enable属性。
我实测下来,RK3568的SPI0在10MHz下驱动AD9833非常稳定,输出频率误差在0.1%以内,完全满足一般测试需求。
5. 常见问题与排查技巧实录
5.1 SPI通信不生效的典型原因
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 无波形输出 | SPI通信失败 | 逻辑分析仪抓SCLK/SDATA/FSYNC |
| 波形频率不对 | 寄存器写入错误 | 检查频率计算和字节序 |
| 波形失真 | 参考时钟不稳 | 示波器测AD9833的MCLK引脚 |
| 设备节点不存在 | 设备树未生效 | 检查/proc/device-tree |
| 驱动未匹配 | compatible不匹配 | 检查modalias和驱动of_match_table |
5.2 设备树配置的常见坑
第一个坑是pinctrl冲突。RK3568的SPI引脚可能与其他功能复用,如果pinctrl配置不正确,SPI信号根本出不来。检查方法是在/sys/kernel/debug/pinctrl/下查看引脚复用状态。
第二个坑是片选极性。有些AD9833模块的FSYNC标注为低有效,但实际板上加了反相器,导致极性反转。这种情况下需要在设备树中加上spi-cs-high属性。
第三个坑是时钟频率过高。虽然AD9833标称支持40MHz,但模块上的走线和连接线可能引入振铃和反射,导致数据错误。降到10MHz通常能解决。
5.3 IIO驱动调试技巧
如果你自己写IIO驱动,有几个调试技巧很实用。
第一,在驱动的probe函数中打印SPI设备的模态别名和频率,确认匹配成功。
第二,使用devm_iio_device_alloc和devm_iio_device_register简化资源管理,避免手动释放。
第三,在write_raw回调中打印写入的通道和值,确认用户态写入被正确解析。
第四,利用iio_push_to_buffers_with_timestamp做数据推送,方便后续做波形扫描。
实操心得:AD9833的寄存器是16位的,但SPI传输时通常按8位字节发送。注意字节序——先发高字节还是低字节,取决于你的驱动实现和AD9833的数据手册要求。我遇到过因为字节序反了导致频率输出为设定值65536倍的情况,排查了半天才发现是这个问题。
5.4 与OpenHarmony设备兼容性测评的关联
如果你要把这个方案用于OpenHarmony的设备兼容性测评,需要注意几点。测评通常要求设备在标准OpenHarmony系统上能正常枚举、驱动加载成功、用户态接口可用。AD9833作为SPI外设,需要确保设备树配置符合OpenHarmony的HDF(Hardware Driver Foundation)规范,或者至少在内核层面能正常工作。
另外,LiteOS-M环境下没有Linux设备树和IIO子系统,AD9833的驱动需要按HDF框架重新实现。这部分内容比较复杂,后续可以单独展开。
6. 进一步扩展:从单芯片到多设备协同
AD9833只是SPI总线上的一个设备。在实际项目中,你可能还需要同时驱动SPI Flash、SPI显示屏、SPI传感器等。这时候设备树的组织方式就很重要了。
我通常会把所有SPI设备按片选编号排列在同一个SPI控制器节点下,每个设备独立配置spi-max-frequency和SPI模式。如果片选不够用,就启用cs-gpios做软件片选。
另外,IIO子系统支持触发器和缓冲区,你可以把AD9833的输出与ADC采集联动,实现扫频测试或闭环控制。这部分在标准Linux下有现成的IIO trigger框架可用,OpenHarmony下需要确认内核配置是否开启了相关选项。
最后分享一个小技巧:在调试SPI时序时,如果手头没有逻辑分析仪,可以用内核的spi-debugfs接口抓取传输记录。挂载debugfs后,在/sys/kernel/debug/spi/下可以看到每次传输的字节数和时间戳,对排查通信问题很有帮助。