ADC 这个外设在嵌入式圈子里算是“老熟人”了,但真要把它从裸机寄存器一路写到 Linux 内核驱动,中间踩的坑能装满一箩筐。我做过不少基于 ARM 平台的项目,从 STM32 的裸机采样到 i.MX6ULL 上的 IIO 子系统驱动,每次重新梳理 ADC 的编写方法,都会发现一些之前没注意到的细节。这篇文章就把 ADC 裸机编写和 Linux 驱动编写这两条路线彻底讲清楚,从原理到代码、从寄存器到设备树、从采样精度到常见故障排查,尽量做到一篇看完就能上手实操。不管你是刚接触 ADC 的新手,还是已经写过几版驱动但总觉得不够稳的老手,下面这些内容应该都能帮你省下不少调试时间。
1. ADC 基础原理与两种开发路线的本质差异
1.1 ADC 到底在做什么:从模拟电压到数字量的转换逻辑
ADC 的全称是 Analog-to-Digital Converter,模数转换器。它的核心任务就一件事:把一个连续变化的模拟电压信号,转换成一个离散的数字量,让 CPU 或 MCU 能够读取和处理。听起来简单,但实际转换过程中涉及采样、量化、编码三个步骤,每一步都有讲究。
采样是把连续时间信号变成离散时间信号,这里就引出了采样定理——采样频率必须大于信号最高频率的两倍,否则会出现混叠。比如你要采集一个 1kHz 的正弦波,采样率至少得 2kHz 以上,实际工程中一般取 5 到 10 倍才够用。量化是把采样后的电压值映射到有限的数字刻度上,一个 12 位 ADC 有 4096 个刻度,参考电压 3.3V 时,每个刻度对应 3.3V/4096 ≈ 0.806mV。编码就是把量化结果输出成二进制数。
理解这三个步骤非常关键,因为后面裸机编程和 Linux 驱动编程中遇到的绝大多数问题,比如采样值跳动、精度不够、转换速度慢,根源都在这里。很多人一上来就抄代码,结果采样值一直不稳,查了半天发现是采样时间设置太短,保持电容还没充够电就启动转换了。
1.2 裸机与 Linux 驱动:两条路线的适用场景与核心区别
裸机开发 ADC 和基于 Linux 驱动开发 ADC,本质上是两种完全不同的思维方式。
裸机开发是直接操作寄存器,你拥有对硬件的完全控制权。代码跑在 MCU 上,没有操作系统调度,没有虚拟内存,没有中断延迟的不确定性。你写什么,硬件就执行什么。这种方式适合对实时性要求高、成本敏感、功能相对固定的场景,比如电机控制、传感器采集节点、简单的数据记录仪。裸机 ADC 的代码量通常不大,一个初始化函数加一个读取函数就能搞定,但你需要自己处理所有细节:时钟使能、引脚配置、采样时间、触发方式、中断或 DMA 传输。
Linux 驱动开发则是另一套逻辑。Linux 内核已经为你准备好了 ADC 子系统框架——IIO(Industrial I/O)子系统。你需要做的是按照框架的规范,注册设备、实现回调函数、配置设备树,让内核知道你的 ADC 硬件长什么样、怎么访问。好处是用户空间可以通过统一的 sysfs 接口或字符设备读取数据,不用关心底层寄存器。代价是你得理解 Linux 设备模型、设备树语法、内核模块编译、probe 函数的执行流程。这种方式适合复杂的嵌入式 Linux 系统,比如工业网关、智能终端、需要多任务并发的数据采集平台。
| 对比维度 | 裸机开发 | Linux 驱动开发 |
|---|---|---|
| 硬件控制 | 直接操作寄存器,完全掌控 | 通过内核框架间接控制 |
| 实时性 | 确定性高,微秒级响应 | 受调度影响,毫秒级常见 |
| 代码复杂度 | 低,几百行搞定 | 高,涉及设备树、内核模块 |
| 可移植性 | 差,换芯片基本重写 | 好,框架统一,改设备树即可 |
| 适用场景 | MCU、RTOS、专用采集 | 嵌入式 Linux、多任务系统 |
| 调试手段 | 示波器、串口打印 | dmesg、sysfs、逻辑分析仪 |
选择哪条路线,取决于你的项目需求。如果只是采集几个通道的电压值,MCU 裸机足够了。如果需要接入网络、存储、多用户访问,Linux 驱动是更合理的选择。我个人的经验是,先把裸机搞明白,再去写 Linux 驱动,会顺畅很多,因为底层硬件的时序和寄存器逻辑是相通的。
2. 裸机 ADC 编写:从寄存器到可用的采样代码
2.1 硬件准备与关键参数确认
动手写代码之前,有几件事必须先确认清楚,否则后面调试会很痛苦。
第一,参考电压。ADC 的转换精度直接依赖参考电压的稳定性。大部分 MCU 的 ADC 参考电压可以选择内部参考(比如 1.2V、2.5V)或外部参考(VDDA)。如果对精度要求高,建议用外部基准芯片,比如 REF3033 提供 3.3V 基准,温漂小、噪声低。用 VDDA 做参考时,要注意电源纹波,ADC 对电源噪声非常敏感。
第二,分辨率。常见的有 8 位、10 位、12 位、16 位。分辨率越高,能分辨的最小电压变化越小,但对噪声的要求也越高。12 位 ADC 在 3.3V 参考下,LSB 约 0.8mV,如果电源噪声有 5mV,那低几位基本就是跳动的。
第三,采样时间。这是最容易被忽视的参数。ADC 内部有一个采样保持电容,采样时间太短,电容充电不充分,转换结果就会偏小。采样时间需要根据信号源的内阻来计算。一般来说,信号源内阻越大,需要的采样时间越长。STM32 的参考手册里有一个公式:采样时间 ≥ (R_source + R_ADC) × C_ADC × ln(2^(N+2)),其中 N 是分辨率。实际工程中,我通常先用一个较大的采样时间(比如 239.5 个 ADC 时钟周期),确认采样值稳定后再逐步缩短。
第四,触发方式。ADC 可以由软件触发、定时器触发、外部中断触发。软件触发最简单,适合低速采样。定时器触发适合需要固定采样率的场景,比如音频采集。外部触发适合与外部事件同步的场合。
2.2 寄存器级初始化流程与代码实现
以常见的 ARM Cortex-M 系列 MCU 为例,裸机 ADC 的初始化流程大致如下。不同芯片的寄存器名称不同,但逻辑是相通的。
第一步,使能 ADC 时钟。在 RCC 寄存器中打开 ADC 外设的时钟,同时别忘了使能对应的 GPIO 时钟。
第二步,配置 GPIO 为模拟输入模式。这一步很关键,很多人忘了把引脚设成模拟模式,结果采样值一直是 0 或者满量程。模拟模式下,GPIO 的数字输入输出功能被关闭,引脚直接连到 ADC 的模拟输入端。
第三步,配置 ADC 的时钟分频。ADC 的工作时钟不能超过手册规定的最大值,通常 14MHz 左右。如果系统时钟是 72MHz,就需要 6 分频。
第四步,设置采样时间和通道。每个通道可以独立设置采样时间,根据信号源内阻选择合适的值。
第五步,配置转换序列和触发方式。单通道单次转换最简单,多通道扫描模式需要设置规则组的通道顺序。
第六步,使能 ADC 并校准。很多 MCU 的 ADC 需要先执行一次校准,否则精度会受影响。校准通常在 ADC 使能之后、开始转换之前进行。
下面是一段典型的初始化代码结构,以 STM32F1 系列为例:
void ADC1_Init(void) { // 1. 使能时钟 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 2. 配置 PA0 为模拟输入 GPIOA->CRL &= ~(0x0F << 0); GPIOA->CRL |= (0x00 << 0); // 模拟输入模式 // 3. ADC 时钟分频,PCLK2/6 RCC->CFGR &= ~RCC_CFGR_ADCPRE; RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6; // 4. 设置采样时间,通道0,239.5周期 ADC1->SMPR2 &= ~(0x07 << 0); ADC1->SMPR2 |= (0x07 << 0); // 5. 规则组配置,单通道 ADC1->SQR1 &= ~ADC_SQR1_L; ADC1->SQR3 &= ~ADC_SQR3_SQ1; ADC1->SQR3 |= (0 << 0); // 通道0 // 6. 使能 ADC 并校准 ADC1->CR2 |= ADC_CR2_ADON; for(volatile int i = 0; i < 10000; i++); // 等待稳定 ADC1->CR2 |= ADC_CR2_CAL; while(ADC1->CR2 & ADC_CR2_CAL); }读取转换结果的函数也很简单:
uint16_t ADC1_Read(void) { ADC1->CR2 |= ADC_CR2_ADON; // 启动转换 while(!(ADC1->SR & ADC_SR_EOC)); // 等待转换完成 return ADC1->DR & 0xFFFF; // 读取结果 }这段代码看起来简单,但有几个坑我踩过。第一,校准之前必须确保 ADC 已经使能并且稳定,否则校准结果不准。第二,ADC_CR2_ADON在某些芯片上第一次写是使能 ADC,第二次写才是启动转换,这个细节要看具体手册。第三,读取DR寄存器会自动清除EOC标志,如果用了 DMA,这个行为会不一样。
2.3 采样数据处理:滤波、去直流与归一化
裸机采到的原始值往往不能直接用,需要做几件事:滤波、去直流、归一化。
滤波是最常见的需求。ADC 采样值总会有噪声,简单的做法是多次采样取平均。比如连续采 16 次,去掉最大值和最小值,剩下的取平均。这种方法叫“去极值平均滤波”,对脉冲噪声特别有效。代码实现如下:
#define SAMPLE_TIMES 16 uint16_t ADC1_ReadFiltered(void) { uint32_t sum = 0; uint16_t max = 0, min = 0xFFFF; uint16_t buf[SAMPLE_TIMES]; for(int i = 0; i < SAMPLE_TIMES; i++) { buf[i] = ADC1_Read(); if(buf[i] > max) max = buf[i]; if(buf[i] < min) min = buf[i]; sum += buf[i]; } sum -= max; sum -= min; return sum / (SAMPLE_TIMES - 2); }如果对实时性要求高,可以用一阶低通滤波,计算量小,效果也不错:
float filtered = 0.0f; float alpha = 0.1f; // 滤波系数,越小越平滑 filtered = alpha * new_sample + (1 - alpha) * filtered;去直流是信号处理中的常见操作。比如你要采集一个叠加在 1.65V 直流偏置上的交流信号,就需要把直流分量去掉,只保留交流部分。做法很简单:计算一段时间内的平均值作为直流分量,然后用每个采样值减去这个平均值。
归一化是把 ADC 值转换成实际的物理量。比如 12 位 ADC,参考电压 3.3V,那么电压值 = ADC值 × 3.3 / 4096。如果后面还有放大器,还要除以放大倍数。归一化之后的数据才有物理意义,方便后续处理和显示。
注意:滤波会引入延迟,去极值平均滤波需要采 16 次才能输出一个值,如果采样率是 1kHz,输出率就只有 62.5Hz。实时控制场景要权衡滤波强度和响应速度。
2.4 裸机 ADC 的进阶用法:DMA 与定时器触发
单次软件触发采样适合低速场景,但如果要连续采集大量数据,比如音频或振动信号,就需要 DMA 和定时器触发了。
DMA 的作用是在 ADC 转换完成后自动把数据搬到内存,不需要 CPU 干预。配置好 DMA 通道后,ADC 每完成一次转换就触发一次 DMA 请求,数据直接写入指定的缓冲区。这样 CPU 可以去做其他事情,等缓冲区满了再统一处理。
定时器触发则是用定时器的更新事件或比较事件来启动 ADC 转换,这样可以获得非常精确的采样率。比如你要 10kHz 采样率,就把定时器配置成 10kHz 的更新频率,触发源选择对应的定时器事件。
这两者结合,就是嵌入式数据采集的经典方案:定时器触发 ADC,ADC 通过 DMA 搬运数据,CPU 只负责处理缓冲区。我做过一个振动监测项目,用这套方案实现了 50kHz 连续采样,CPU 占用率不到 5%。
配置 DMA 时要注意几点:DMA 的传输宽度要和 ADC 数据宽度匹配(通常是半字,16 位),DMA 模式要选循环模式以便连续采集,ADC 要配置成连续转换模式。另外,DMA 传输完成中断里不要做太耗时的操作,否则会丢数据。
3. Linux 驱动编写:基于 IIO 子系统的 ADC 驱动开发
3.1 Linux IIO 子系统架构与 ADC 驱动模型
Linux 内核的 IIO 子系统是专门为 ADC、DAC、陀螺仪、加速度计等模拟器件设计的框架。它的核心思想是:把设备的共性抽象出来,驱动开发者只需要实现硬件相关的部分,用户空间通过统一的接口访问数据。
IIO 子系统的架构大致分为三层。最底层是硬件层,也就是你的 ADC 芯片或 MCU 内部的 ADC 模块。中间是驱动层,你编写的驱动代码在这里注册到 IIO 框架,实现iio_info结构体中的回调函数。最上层是用户接口层,内核自动为你生成 sysfs 节点,比如/sys/bus/iio/devices/iio:device0/in_voltage0_raw,用户空间直接读这个文件就能拿到 ADC 原始值。
一个典型的 ADC IIO 驱动需要实现以下几个部分:
- 设备树匹配:定义
compatible字符串,让内核知道这个驱动对应哪个硬件。 - probe 函数:设备匹配成功后调用,负责初始化硬件、注册 IIO 设备。
- iio_info 结构体:包含
read_raw回调,用户空间读取数据时调用。 - 通道定义:描述 ADC 有哪些通道、每个通道的类型和索引。
这种架构的好处是标准化。不管你用的是 NXP 的 ADC、TI 的 ADS 系列,还是 STM32 内部的 ADC,用户空间的访问方式都是一样的。换硬件只需要改设备树,应用程序几乎不用动。
3.2 设备树配置:描述硬件连接与通道信息
设备树是 Linux 驱动开发的入门门槛,也是很多人卡住的地方。它的作用是告诉内核:这个 ADC 挂在哪个总线上、寄存器地址是多少、有哪些通道、参考电压是多少。
以 SPI 接口的 ADC 芯片为例,设备树节点大概长这样:
&spi1 { status = "okay"; adc@0 { compatible = "vendor,adc-chip"; reg = <0>; spi-max-frequency = <1000000>; vref-supply = <&vref_3v3>; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; #address-cells = <1>; #size-cells = <0>; channel@0 { reg = <0>; label = "voltage0"; }; channel@1 { reg = <1>; label = "voltage1"; }; }; };几个关键点解释一下。compatible必须和驱动中的of_device_id表匹配,否则 probe 不会被调用。reg是片选号,SPI 设备用这个来区分同一总线上的多个设备。vref-supply指向参考电压 regulator,驱动可以通过 regulator 框架获取参考电压值,用于计算实际电压。interrupts配置中断引脚,如果 ADC 支持转换完成中断,可以用中断方式通知数据就绪。
通道节点是可选的,有些驱动在代码里静态定义通道,有些从设备树动态解析。动态解析更灵活,推荐使用。
注意:设备树中的
compatible字符串一定要和驱动代码完全一致,包括大小写和连字符。我见过有人写成vendor,ADC-chip,结果驱动死活加载不上,查了半天才发现是大小写问题。
3.3 驱动核心代码:probe 函数与 read_raw 回调实现
驱动代码的核心是probe函数和read_raw回调。下面是一个简化的 SPI ADC 驱动框架:
#include <linux/iio/iio.h> #include <linux/iio/sysfs.h> #include <linux/spi/spi.h> #include <linux/regulator/consumer.h> struct adc_chip { struct spi_device *spi; struct regulator *vref; struct mutex lock; }; static int adc_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { struct adc_chip *chip = iio_priv(indio_dev); u8 tx_buf[3], rx_buf[3]; int ret; switch (mask) { case IIO_CHAN_INFO_RAW: mutex_lock(&chip->lock); tx_buf[0] = 0x06 | (chan->channel >> 2); tx_buf[1] = (chan->channel & 0x03) << 6; tx_buf[2] = 0x00; ret = spi_write_then_read(chip->spi, tx_buf, 3, rx_buf, 3); mutex_unlock(&chip->lock); if (ret < 0) return ret; *val = ((rx_buf[1] & 0x0F) << 8) | rx_buf[2]; return IIO_VAL_INT; case IIO_CHAN_INFO_SCALE: *val = regulator_get_voltage(chip->vref) / 1000; *val2 = 12; return IIO_VAL_FRACTIONAL_LOG2; default: return -EINVAL; } } static const struct iio_info adc_info = { .read_raw = adc_read_raw, }; static int adc_probe(struct spi_device *spi) { struct iio_dev *indio_dev; struct adc_chip *chip; int ret; indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*chip)); if (!indio_dev) return -ENOMEM; chip = iio_priv(indio_dev); chip->spi = spi; mutex_init(&chip->lock); chip->vref = devm_regulator_get(&spi->dev, "vref"); if (IS_ERR(chip->vref)) return PTR_ERR(chip->vref); ret = regulator_enable(chip->vref); if (ret) return ret; indio_dev->name = "adc-chip"; indio_dev->info = &adc_info; indio_dev->modes = INDIO_DIRECT_MODE; indio_dev->channels = adc_channels; indio_dev->num_channels = ARRAY_SIZE(adc_channels); return devm_iio_device_register(&spi->dev, indio_dev); } static const struct of_device_id adc_of_match[] = { { .compatible = "vendor,adc-chip" }, { } }; MODULE_DEVICE_TABLE(of, adc_of_match); static struct spi_driver adc_driver = { .driver = { .name = "adc-chip", .of_match_table = adc_of_match, }, .probe = adc_probe, }; module_spi_driver(adc_driver); MODULE_LICENSE("GPL");这段代码里有几个关键设计。iio_priv用于获取驱动私有数据,避免全局变量。mutex保护 SPI 访问,因为 SPI 总线是共享的,多个通道同时读取时需要串行化。IIO_CHAN_INFO_SCALE返回参考电压和分辨率,用户空间可以用这个值把原始值转换成电压。
read_raw回调中,IIO_CHAN_INFO_RAW返回原始 ADC 值,IIO_CHAN_INFO_SCALE返回缩放系数。用户空间读取in_voltage0_raw得到原始值,读取in_voltage0_scale得到缩放系数,两者相乘就是实际电压。
3.4 用户空间访问:sysfs 接口与应用程序读取
驱动加载成功后,用户空间可以通过 sysfs 访问 ADC 数据。先找到设备节点:
ls /sys/bus/iio/devices/ # 输出类似:iio:device0然后读取原始值和缩放系数:
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw # 输出:2048 cat /sys/bus/iio/devices/iio:device0/in_voltage0_scale # 输出:0.805664实际电压 = 2048 × 0.805664mV ≈ 1650mV。
在应用程序中,可以用标准的文件操作读取:
int read_adc_raw(const char *path) { int fd, ret; char buf[32]; fd = open(path, O_RDONLY); if (fd < 0) { perror("open"); return -1; } ret = read(fd, buf, sizeof(buf) - 1); close(fd); if (ret < 0) { perror("read"); return -1; } buf[ret] = '\0'; return atoi(buf); }如果需要连续高速采样,sysfs 的单次读取方式效率太低。这时候可以用 IIO 的缓冲区功能,通过/dev/iio:device0字符设备读取,配合触发器和 DMA,实现高速连续采集。配置过程稍微复杂一些,需要设置触发器和缓冲区长度,但性能提升非常明显。
4. 常见问题排查与实战避坑指南
4.1 裸机 ADC 常见问题速查
裸机 ADC 调试中遇到的问题,八成集中在以下几类。我整理了一个速查表,方便对照排查。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 采样值始终为 0 | GPIO 未配置为模拟模式 | 检查 GPIO 模式寄存器 | 设置为模拟输入 |
| 采样值始终为满量程 | 参考电压异常或通道配置错误 | 测量 VREF 引脚电压 | 检查参考电压和通道号 |
| 采样值跳动大 | 采样时间太短或电源噪声 | 增大采样时间观察 | 增加采样时间,加滤波电容 |
| 多通道数据串扰 | 采样时间不足或通道切换太快 | 单独测试每个通道 | 增加通道间延时 |
| 转换速度慢 | ADC 时钟分频过大 | 检查 ADC 时钟频率 | 调整分频,不超过手册上限 |
| DMA 数据错位 | DMA 宽度或地址配置错误 | 检查 DMA 配置寄存器 | 匹配数据宽度和地址 |
其中“采样值跳动大”是最常见的问题。除了采样时间和电源噪声,还有一个容易被忽视的原因:信号源内阻过大。如果信号源内阻是 100kΩ,而采样时间只有几个周期,保持电容根本充不满。解决办法要么增大采样时间,要么在信号源和 ADC 之间加一个电压跟随器,降低输出阻抗。
4.2 Linux ADC 驱动调试技巧
Linux 驱动调试和裸机完全不同,你面对的是一个庞大的内核系统。以下是我总结的几个实用技巧。
第一,用 dmesg 看 probe 是否成功。驱动加载后,第一时间执行dmesg | tail,看看有没有 probe 成功的打印,或者报错信息。如果 probe 失败,常见原因是设备树 compatible 不匹配、regulator 获取失败、SPI 通信失败。
第二,检查 sysfs 节点是否生成。如果/sys/bus/iio/devices/下没有对应的设备节点,说明 IIO 设备注册失败。这时候要回头检查iio_device_register的返回值。
第三,用逻辑分析仪抓 SPI 波形。如果 sysfs 能读到值但数据不对,大概率是 SPI 通信问题。用逻辑分析仪抓一下 CS、CLK、MOSI、MISO 四根线,对照芯片手册的时序图,看看命令字节和数据字节是否正确。
第四,注意内核版本差异。IIO 子系统的 API 在不同内核版本之间有变化,比如iio_device_alloc在旧版本中需要传入sizeof(struct adc_chip),新版本用devm_iio_device_alloc更安全。写驱动前先确认目标内核版本,参考对应版本的内核源码中的示例驱动。
第五,regulator 的坑。如果设备树中配置了vref-supply,但驱动中调用regulator_get失败,检查 regulator 节点是否使能、电压范围是否匹配。有些平台的 regulator 需要显式设置regulator-always-on或regulator-boot-on。
提示:调试 Linux 驱动时,可以在 probe 函数中加
dev_info打印,比printk更规范,日志中会带上设备名称,方便过滤。
4.3 精度优化与抗干扰实战经验
ADC 的精度问题,一半靠硬件,一半靠软件。硬件方面,参考电压的稳定性是重中之重。我做过对比测试,用 MCU 内部参考和外部基准芯片,同样的 12 位 ADC,外部基准的采样值跳动范围能缩小到内部的五分之一。如果项目对精度有要求,外部基准芯片是必须的。
电源去耦也很关键。ADC 的电源引脚旁边一定要放 100nF 和 10uF 的电容,越靠近引脚越好。模拟地和数字地要分开走线,最后在一点汇合。如果板子上有开关电源,尽量让 ADC 远离电感和大电流走线。
软件方面,前面提到的去极值平均滤波是最实用的。如果采样率允许,还可以用中值滤波,对脉冲干扰的抑制效果更好。对于周期性噪声,比如 50Hz 工频干扰,可以用同步采样法,让采样率是工频的整数倍,这样干扰在平均后会被抵消。
还有一个技巧是过采样。如果 ADC 是 12 位,但你只需要 10 位的精度,可以采 16 次求平均,等效分辨率能提高 2 位。原理是每次采样的噪声是随机的,平均之后噪声被抑制,有效位数提高。过采样倍数每增加 4 倍,分辨率提高 1 位。
5. 从裸机到 Linux:ADC 开发的进阶路线建议
如果你已经掌握了裸机 ADC 的编写,想进一步学习 Linux 驱动开发,我的建议是分三步走。
第一步,先熟悉 Linux 内核模块的基本框架。写一个最简单的 hello world 模块,学会编译、加载、卸载,理解module_init和module_exit的机制。这一步不难,但必须动手做一遍。
第二步,找一个现成的 IIO 驱动源码,对照芯片手册逐行阅读。推荐从内核源码drivers/iio/adc/目录下找一个简单的驱动,比如ti-adc081c.c或max1363.c,这些驱动代码量不大,逻辑清晰,适合入门。
第三步,在自己的开发板上移植一个 SPI 或 I2C 接口的 ADC 芯片。从设备树配置开始,到驱动编写、编译、加载、调试,完整走一遍流程。遇到问题不要怕,dmesg 和逻辑分析仪是你最好的朋友。
裸机和 Linux 驱动并不是对立的,而是互补的。理解了裸机层面的寄存器操作和时序逻辑,写 Linux 驱动时你会更清楚每一步在做什么。反过来,Linux 驱动中的框架思维和标准化接口设计,也能让你在裸机开发中写出更规范、更易维护的代码。
我个人在实际操作中的体会是,ADC 这个东西,看再多文档不如动手焊一块板子、写一版代码、用示波器看一次波形。采样值跳动的时候,不要急着改代码,先想想硬件层面有没有问题。参考电压稳不稳、地线干不干净、信号源内阻大不大,这些往往才是根源。软件滤波能解决一部分问题,但解决不了硬件设计缺陷。把硬件基础打牢,软件调试会顺利很多。