编写用于模数转换器的软件驱动程序

分享到:

作者:凌力尔特公司混合信号产品应用经理Mark Thoren  应用工程师Leo Chen

“你能发送给我一个用于……的驱动程序吗?”

模数转换器 (ADC) 提供了一个进入模拟世界的嵌入式控制器窗口。选择合适的 ADC 可以去掉一个电路板的运算放大器和调节电位器,从而产生一个干净、坚固的设计。设计过程需要确保满足电路的模拟需求,确保处理器能够用 ADC 的输出数据做有用的事,两者的粘合剂是低级驱动程序代码。

与存储器、UART、I/O 扩展器等纯数字外部设备相比,模数转换器写固件时需要特殊考虑。如果要分析一个 AC 信号,那么转换开始的定时必须绝对确定,而且不受软件引起的抖动影响,从而对控制器如何启动转换提出了严格要求。用于 DC 测量的高分辨率 ADC 有可能具有一个比控制器必须执行的任何其他过程都要长得多的转换时间。最后,数字接口可能与控制器的内置外部设备控制器不是完美匹配的。
 
本文描述了一种编写用于 LTC2499 (一款具 I2C 接口的 24 位、ΔΣ ADC) 的驱动程序的步进法。它举例说明了驱动程序设计由于诸多原因而存在的的许多微妙之处。

1) 与微控制器中发生的大多数事情相比,转换时间非常长。
2) 它使用 I2C 接口,这种接口内置在很多控制器中,但是每个控制器都有自己特定的实现方式。
3) I2C 接口本身用来指示转换的状态,而不是状态位。
4) 它有多个输入通道,从而需要在固件中恰当排序。


ADC 详细信息

一个 ADC 必须做两件事:接受模拟测量值;将数据发送到控制器。两件事可能同时发生 (取决于 ADC 的架构)。例如,12 位 LTC2366 用串行数据时钟进行转换。就高速器件而言,这是有意义的,同时进行的操作越多,采样就越快。而 LTC2499 采用增量累加架构,以实现精确 DC 规范,结果是长的转换时间 (145ms)。

LTC2499 用 I2C 接口实现配置和数据传送。这个接口以 100kHz 和 400kHz 的标准速率工作,这相当于用大约 112.5us 至 450us 读一次转换结果 (发送7位地址和读出位;接收4个数据字节,在每个字节的末端是一个ACK/NACK位)。这意味着,该 ADC 根本不能与控制器可能的通信速度一样快地产生结果。好消息是:借助少量规划,控制器能够运用转换时间来完成其他有用的操作,比如:捣弄数字或显示数据。

该ADC还通过不确认其I2C地址来向控制器传送“某项转换操作正在进行之中”的信息。一些 EEPROMS 在写周期也用这种方法来指示写操作正在进行中,这个操作也许需要数 10ms。

另一项将在开发后期节省大量时间的工作是,开始定义一些有用的常数。负责对ADC进行配置的两个字节在A0和EN2位之间被方便地分离了。你可以从定义地址选择字节开始,以供输入用来测试 (在这种情况下,是在 CH0 和 CH1 之间的差分测量),以及定义配置,以在 50/60Hz 抑制模式测量输入 (而不是内部温度传感器),如表 1 所示。

#define CH0_1_DIFF  0xA0 // First config byte to select CH0-1 differential
#define EXT_1X_50_60HZ 0x80 // Second config byte to select analog inputs
#define TEMP_50_60HZ 0xA0 // Second config byte to select temperature sensor

表 1 : 字符配置
 
微控制器详细信息

很多微控制器都有内置外部设备,用于 I2C 通信。这些硬件将常常实现一些任务的自动化,如产生启动和停止条件、发送和接收数据字节、以及在传送未确认时发出提醒信息。这些任务常常是通过给一个寄存器装入数据来启动的,然后在应该发送数据的时候确认一个数据位;当任务完成或发生错误时,将产生一个中断信号。这缩小了指令速率 (每 us 很多条指令) 与 I2C 总线工作速率的差别;分别就 100kHz 和 400kHz 标准时钟速率而言,每个 I2C 时钟周期为 2.5us 至 10us,或一次 9 位数据传送为 22.5us 至 90us。

避免浪费一个指令周期、完全采用彻底的中断驱动方法,这是很吸引人的。但是有很多事情可能出错,因此一种安全的方法是,从启动一个任务开始,然后等待这个任务完成,之后再继续。

当决定是否转换到中断驱动的 I2C 接口时,你应该在所增加的复杂性和效率之间仔细权衡。在一个以400kHz频率运行I2C总线的慢速处理器中,与简单地通过轮询来完成每项操作相比,由于进入和退出中断服务例程所引起的开销有可能使工作效率出现下降。此外,如果数据没有正在通过 I2C 端口读取,主程序就不能继续,那么它也许根本没有任何有用的事情可做。

不过,一个中断驱动的驱动程序的确有一些优势。等待转换完成的时间可以用来捣弄数字或刷新显示器。重要的事情是,固件用以下方式恰当地以流水线方式传送数据:数据在有效之前不被使用。不过,这超出了本文的范围,因此,在本文的余下部分,我们将集中探讨不依靠中断的固件。


组合 ADC 与微控制器

与 I2C 外部设备通信的步骤视器件的不同而有所改变,这里存在很多陷阱。该数据表显示事件序列,我们需要这些事件来确认转换完成、为下一次转换写配置、读数据和启动下一次转换。用诸如图 1 中所示的数码作为“伪代码”,并直接转换成 C 语言。

图 1:你可以非常容易地将一个数据表数据序列转换成可用代码


在执行该代码的过程中,您期望读回构成ADC转换结果的4个数据字节。请注意,全零不构成 LTC2499 的有效输出,因此一个零返回码可以用来指示转换正在进行中。一个适合捣弄数字的地方就是立即在从 read_LTC2499 读一个有效结果之后。这是因为对最后一个数据字节的读操作触发下一次转换,ADC 将忙于执行为时 145ms 的下一次转换。

在尝试中断已经被读回的字节之前,用模拟示波器 (而不是用逻辑分析仪) 看数字 SDA 和 SCL 信号是个好主意。尽管这些是“数字” 信号,但是很大一部分错误实际上是模拟的。即使你复核过,处理器引脚是正确配置的,仍然要确认两个信号始终没有拉低。检查一下,保证安装了正确的上拉电阻,常见的是 5k 或 10k,不过最佳值取决于总线电容和其它因素。图 2 中上面的曲线显示 50k 上拉电阻的结果 (上升时间过长,下降时间尚可),下面的曲线显示 100Ω 电阻的结果 (上升时间和下降时间都很短,但是低电平太高;甚至可能损坏处理器和/或外部设备引脚。)

图 2:上面的曲线是过大的 (50k) 上拉电阻的结果,下面的曲线是太小的 (100Ω) 上拉电阻的结果。


SDA 和 SCL 是两个单独的单端信号 (不是一个差分对)。图 3 显示如果数据线非常长且相邻很近时 (在这种情况下,是 120cm 的 CAT-5 双绞线和 5k 上拉电阻) 会发生什么变化。上面的曲线应该是在恒定的高电平上,然而却显示出与下面的曲线在上升沿和下降沿上的过度耦合。图 4 显示由短的走线和 5k 上拉电阻所产生的可接受波形。

图 3:这些曲线显示,在相邻很近、很长的 (120cm) 传输线上,信号会发生什么变化。

 

图 4:一个可接受的 I2C 波形


物理接口工作正常以后,要将注意力转向实际的转换数据。使用 ADC 的良好开端是,加上一个已知输入,寻找预期输出。首先,将 ADC 设置成在 Channel 0 - 1 上差动转换,发送之前定义的配置字节,然后读出转换结果。

在使用 LTC2499 的情况下,将 CH1 引脚连接到地,CH0 引脚连接到 5V,应该产生正的过量程代码 0xC0000000。以相反方式连接,应该产生一个输出代码 0x3FFFFFFF。而将两个输入都接地 (零差分输入) 应该产生一个接近0x80000000的代码。

到这个阶段以后,一种非常省心的方法是,编写一个函数,将 ADC 代码转换成可读的电压。该器件的数据格式是偏移二进制;即:当输入为零时,输出为 231。(之所以是 31,原因是LTC2499 除了输出 24 个数据位和一个符号位之外,还输出了子 LSB (最低有效位))。采用带符号整数算法将该偏移从 ADC 转换结果中减去,将产生一个具 +/-230 范围的二进制补码字 (当输入电压从负全标度变至正全标度时)。因此:

输入电压 = 基准电压 x (ADC 代码 - 231) / 231

以伏特为单位显示 ADC 的输出,你就可以在“真实的模拟世界”中比较转换结果和数字电压表输出。请注意,基准电压为 5V 时,LTC2449 的输入范围是 –Vref/2 至 +Vref/2,或 +/-2.5V。


扫描通道

用多通道 ADC 完成的一个常见的任务是,重复扫描一个预先确定的通道序列。完成这个任务的一个方便方法是,设置大量通道地址以及一个阵列来存储接收的数据,用 1 偏移 (请记住:你正在从上一次转换读出数据,并为下一次转换写通道配置)。图 5 中的电路给各个差分通道施加了一个电压“阶梯”。这使你能够非常方便地证实,你正在选择正确的通道,并正在向正确的单元存储数值。

 

图 5:一个“阶梯电路”为你提供一串级联的电压,以测试固件的可读性。


表 2 显示的内容包括:Channel Array 常数、来自结果表的 16 进制数值以及利用上面所写的输入电压等式将这些数值转换成的电压值:

result = read_LTC2499(0xA0, EXT_1X_50_60HZ);
 while(result==0){    //Loop until conversion done
  result = read_LTC2499(0xA0, EXT_1X_50_60HZ);
 }

for(i=0; i<8; ++i)
 {
 result = read_LTC2499(Channel_Array(i), EXT_1X_50_60HZ);
 while(result==0){    //Loop until conversion done
  result = read_LTC2499(Channel_Array(i), EXT_1X_50_60HZ);
 }
Result_table(i) = result;
 }


关于这个表和这段代码,有两件重要的事情要注意。第一个是发生在主循环之前的“虚拟转换”。这种转换的目的是,用正确的设置值装载 ADC,以实现首次有效转换;读回的数据不存储到 Result_table中。请记住,每次给 ADC 一个读命令,它都输出上一次发生的转换结果,而不是正在设置的这一次转换的结果。要注意的第二件事情是,Channel_Array 数据实际上是怎样由 1 偏移的。这也是使“虚拟转换”成为必要的 ADC 读操作的一个直接结果。


表 2 : 从阶梯电路的电压
 


结论

与 ADC 通信给编写嵌入式程序的程序员带来了新的挑战。人们应该采用一种允许硬件和软件单独调试的系统化方法。要产生一个高效率和便于集成到应用代码中的驱动程序,彻底了解 ADC 的运行是必要的。