[其它产品/技术] 没有 DAC?没问题:在CLB中构建DAC(翻译转)

[复制链接]
953|8
幸福小强 发表于 2025-8-11 11:56 | 显示全部楼层 |阅读模式
使用PIC16F13145中的可配置逻辑块 (CLB) 构建了一个 16 位数模转换器 (DAC)。只需低于独立DAC的成本(不到50美分),您就可以实现智能、灵活、高分辨率的DAC,同时仍保留对丰富的板载外设集的访问。

什么是数模转换器?
数模转换器 (DAC) 允许微控制器和其他数字设备与模拟世界连接。它将数字值(例如代表电压的二进制数)转换为连续的模拟输出,例如电压或电流。它用作模数转换器 (ADC) 的对应物。

方法
Delta Sigma 数模转换器
为了实现 DAC,我使用 CLB 创建了一个 Δ-Σ DAC。与电阻梯形或基于 PWM 的 DAC 不同,Δ-Σ DAC 生成 1 位高速数字输出流,然后对其进行滤波以恢复干净的模拟信号。这种方法允许以最少的硬件实现极高的有效分辨率,非常适合嵌入式系统。

我们如何将数字脉冲转换为模拟脉冲?
每个时钟周期,我们都可以驱动一个数字输出引脚为高电平或低电平。通过每两个时钟周期交替状态,平均值为 50%。如果我们过滤这个方波,我们得到一个介于低和高之间的平均电压。

为了生成更多级别,我们可以将其扩展到固定数量的样本。例如,要生成 8 个离散电平(3 位分辨率),我们可以采用 8 个周期段并改变高电平的数个(0 到 8):

这个想法可以扩展到更高的分辨率。对于 10 位(1024 步),我们需要为每个输出值生成并平均 1024 个单独的高/低状态。但这带来了一个挑战:涟漪。即使进行了滤波,如果滤波不充分,开关噪声也会主导信号。

假设我们使用 32 MHz 时钟和 8 周期 PWM 环路。这导致 4 MHz 输出波形。如果以2 MHz(PWM频率的一半)进行滤波,纹波仍为满量程的~60%,对于8位精度来说太高了。为了将纹波降低到 1 LSB 以内(3 位为 12.5%),我们需要低得多的滤波器带宽:

对于高分辨率应用(10+位),PWM变得不切实际——所需的滤波器时间常数对于实际使用来说变得太长。
🔺-Σ
我们怎样才能在相同的时钟输出下做得更好?我们需要使信号中我们不想要的部分更频繁地发生,以便它们更容易被过滤掉。滤波器工作得越好,频率越高(我们切换的频率越高)。

首先,我们从一个系统开始,该系统每个时钟周期都会交换其输出。我们从输出中获得反馈。如果输出低,我们走高,如果输出高,我们走低。

这种配置不是很有用。如果我们想要八个输出怎么办?3 位输出?我们可以跟踪我们发送了多少输出。如果我们将 0 到 7 视为总数的分数,则为 0、0.125、0.25、......1. 例如,我们可能想要输出 4 * 0.125 或 0.5。

我们可以使用 3 位计数器来跟踪状态。每个周期我们都希望输出为 4 * 0.125 或 0.5,因此我们将其添加到跟踪计数器中。当计数器变得如此之大时,我们需要减去满量程,我们可以让它滚动有效地“减去”满量程。

这输出以下模式:与 4 位 PWM 模式相比,它有更多的边,靠得更近。但我们只需要一半的滤波即可获得平坦到 1 LSB 的输出;因为边缘彼此非常接近。

这种优势只会随着您需要的分辨率位数的增加而增加。在 16 位时,使用相同的 32MHz 时钟,您可以获得超过 100,000 倍的带宽。这种效果称为噪声整形,我们有尽可能多的边缘将噪声推到更高(更容易过滤)的频率。




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 幸福小强 发表于 2025-8-11 11:59 | 显示全部楼层
实现
PIC16F13145中的 CLB 提供 32 个 LUT。为了实现 16 位 Σ-Δ DAC,我们使用这些 LUT 构建一个 16 位累加器。Delta-Sigma 的美妙之处在于我们不需要显式比较器或溢出逻辑——让累加器环绕自然地执行必要的减法。进位成为我们的 1 位 DAC 输出。

由于每个加法器级都需要一个求和和一个进位输出,因此每个位消耗两个 LUT。这允许 32 个 LUT 中的 16 位累加器最大限度地发挥 CLB 的容量。

Verilog 参考模型
以下是基于累加器的 delta-sigma DAC 的简单 Verilog 实现:
  1. module dac_16 (
  2.     input        clk,          // bit-stream clock
  3.     input [15:0] din,          // 16-bit input word
  4.     output       out           // 1-bit ΣΔ output
  5. );
  6.     reg [15:0] acc;
  7.     reg        carry;
  8.     always @(posedge clk) begin
  9.         {carry, acc} <= acc + din;
  10.     End
  11.     assign out = carry;
  12. endmodule
多亏了 Verilog 块,您可以将其直接放入 CLB Synthesizer(项目文件)中。

现在您甚至可以模拟设计!

不幸的是,这种设计太复杂,无法在 CLB 合成器中合成(它只能处理大约 40-60% 的满,而不是 100% 的满)。但多亏了我的逆向工程,我可以手动合成它!
  1. from itertools import cycle
  2. from pathlib import Path
  3. from bitstream import Bitstream
  4. from build_lut import LUT4, a, b, c, d
  5. from clb_graph import generate_dot_from_config
  6. from data_model import (_CLB_ENUM, BLE_CFG, BLEXY, FLOPSEL, LUT_IN_A, LUT_IN_B,
  7.                         LUT_IN_C, LUT_IN_D, PPS_OUT_NUM, CLKDIV)

  8. BANKS = {"A": LUT_IN_A, "B": LUT_IN_B, "C": LUT_IN_C, "D": LUT_IN_D}
  9. NAME2BANK = {n: k for k, v in BANKS.items() for n in v.__members__}
  10. SYM = {"A": a, "B": b, "C": c, "D": d}

  11. ble = lambda i: (LUT_IN_A, LUT_IN_B, LUT_IN_C, LUT_IN_D)[i // 8][f"CLB_BLE_{i}"]
  12. swin = lambda i: (LUT_IN_A, LUT_IN_B, LUT_IN_C, LUT_IN_D)[i // 8][f"CLBSWIN{i}"]

  13. bs = Bitstream()

  14. # BLE indices grouped by bank
  15. luts = {b: [i for i in range(32) if NAME2BANK[ble(i).name] == b] for b in "ABCD"}

  16. carry_cycle = cycle("CD")
  17. stages = {
  18.     i: (
  19.         luts["B" if i < 8 else "A"].pop(0),  # SUM LUT
  20.         luts[next(carry_cycle)].pop(0),
  21.     )  # CARRY LUT
  22.     for i in range(16)
  23. }

  24. for bit, (sum_lut, car_lut) in stages.items():
  25.     d_in = swin(bit)
  26.     s_fb = ble(sum_lut)
  27.     taps = [d_in, s_fb] if bit == 0 else [d_in, s_fb, ble(stages[bit - 1][1])]

  28.     va, vb, *vc = [SYM[NAME2BANK[t.name]] for t in taps]  # symbols
  29.     sum_expr = va ^ vb if bit == 0 else va ^ vb ^ vc[0]
  30.     carry_expr = va & vb if bit == 0 else (va & vb) | (vb & vc[0]) | (va & vc[0])

  31.     kws = {f"LUT_I_{NAME2BANK[t.name]}": t for t in taps}
  32.     bs.LUTS[BLEXY(sum_lut)] = BLE_CFG(
  33.         LUT_CONFIG=LUT4(sum_expr).bitstream(), FLOPSEL=FLOPSEL.ENABLE.value, **kws
  34.     )
  35.     bs.LUTS[BLEXY(car_lut)] = BLE_CFG(
  36.         LUT_CONFIG=LUT4(carry_expr).bitstream(),
  37.         FLOPSEL=FLOPSEL.ENABLE.value if bit == 15 else FLOPSEL.DISABLE.value,
  38.         **kws,
  39.     )

  40. # Final sigma‑delta output
  41. fin = stages[15][1]
  42. grp = fin // 4
  43. bs.PPS_OUT[PPS_OUT_NUM[grp]].OUT = _CLB_ENUM[grp](fin % 4)
  44. print(repr(_CLB_ENUM[grp](fin % 4)))
  45. print(generate_dot_from_config(bs))
结果
我用一个简单的 RC 滤波器从外部滤波 DAC 输出。以下是示波器上捕获的模拟输出:

要对其进行编程,您需要做的就是使用CLB1_SWIN_Write16写入 DAC 值。
非常适合添加另一个 DAC 输出以进行调谐、更改设定点等。通过允许 6-8 位以上的分辨率和/或额外的分辨率来补充现有的板载 DAC 和 PWM。输出。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 幸福小强 发表于 2025-8-11 12:00 | 显示全部楼层
该PIC16F13145具有内置 DAC 和 PWM。每种方法各有利弊,通常该 CLB DAC 在速度和性能方面优于 PWM 和内部 DAC。遗憾的是,由于SWIN寄存器的限制,系统的上限约为100 ksps。
 楼主| 幸福小强 发表于 2025-8-11 12:00 | 显示全部楼层


三种方式对比

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 幸福小强 发表于 2025-8-11 12:01 | 显示全部楼层
CLB 为以前在该器件系列上禁止的功能打开了大门。通过完全由LUT构建Delta-Sigma DAC,我们实现了16位输出,超过了内置DAC和基于PWM的模拟一代的性能。



该设计采用零额外硅和基本的 RC 滤波,可提供高分辨率模拟输出,适用于从传感器偏置到流音频的所有内容。
 楼主| 幸福小强 发表于 2025-8-11 12:01 | 显示全部楼层
本帖内容翻译自:https://maker.pro/configurable-logic-block/projects/no-dac-no-problem-building-a-dac-in-the-clb
21mengnan 发表于 2025-8-14 10:25 | 显示全部楼层
原来这东西还可以这么用,太给力了。
lxs0026 发表于 2025-9-30 23:55 | 显示全部楼层
先理清你选择 Δ-Σ 架构的核心优势
lxs0026 发表于 2025-9-30 23:57 | 显示全部楼层
通过时钟周期内的高低电平交替,滤波后得到平均电压”,这是 Δ-Σ DAC 的基础逻辑,但需要拆解 “1 位数字流→模拟信号” 的完整过程,才能理解为什么它能实现 16 位分辨率
您需要登录后才可以回帖 登录 | 注册

本版积分规则

143

主题

1720

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部