ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

GD32H759 ADC/DAC驱动实战:从RT-Thread框架到工业级精度

GD32H759 ADC/DAC驱动实战:从RT-Thread框架到工业级精度 这个系列做到第3篇ADC/DAC 驱动终于要登场了。前面两篇分别把 GD32H759 的时钟树和 RT-Thread 基础线程理清了这一篇聊两个模拟外设ADC 通道的采集和 DAC 通道的输出。做工业控制器的人都知道真正的麻烦往往不在协议解析、不在显示界面而是模拟量这一路采得不准、输出抖了一下后面整个 PID 和质量判断全乱。所以这一篇不打算只贴一段“能跑的驱动”更想把采集链路、输出链路、抗干扰布局和验证方法一起讲透。1. 为什么把 ADC/DAC 单独拿出来工控信号的三种场景1.1 模拟量采集不是“读寄存器”那么简单工业控制里 ADC 的用途大致分三种。第一种是过程量采集比如 4-20mA 的变送器经过采样电阻转成电压或者 0-10V 电压信号直接进 ADC这类信号变化慢但对精度要求高往往要做滤波、校准、软件线性化。第二种是快速模拟量反馈比如电机相电流、开关电源的电感电流这类信号变化快需要同步采样、高频连续采集一般都要 DMA。第三种是触发式采集等待某个事件发生比如过压保护、过流保护ADC 要能迅速锁存一个瞬时值这时候轮询反而跟不上。GD32H759 的 ADC 能力放在工控场景里属于比较够用的那一档但“够用”不等于“直接读就完事”。你读到的每一个数字背后都经过了采样保持电容、逐次逼近比较器、参考电压源、供电噪声这一大串物理路径。任何一个环节出问题代码优化得再好也没有用。所以我一直觉得聊 ADC 驱动不能只看寄存器得从信号链的角度看。1.2 输出模拟量想清楚你是在做信号不是做开关DAC 在工控里常见的是做模拟量输出比如控制变频器的 0-10V 给定、阀门的 4-20mA 电流环、伺服驱动的模拟速度参考。这类应用里DAC 输出的微伏级抖动看起来不起眼但经过外部放大、隔离、功率驱动之后可能直接被放大成明显的转速波动或压力波动。更隐蔽的问题是“上电毛刺”。很多 DAC 在上电瞬间输出会有一段不定态如果你的外部电路没有做钳位或使能控制执行机构可能在开机时猛地动一下。设计驱动的时候必须把 DAC 的使能时序、输出缓冲、外部放电电路一起考虑进去而不是写完rt_dac_write就算完。这三种场景对应着三种驱动写法慢速高精度走框架轮询加重采样快速连续采集走 DMA需要闭环控制的用定时器触发 ADC/DAC让转换周期和 PWM 周期精确对齐。接下来我按实际落地顺序把这些展开。2. RT-Thread 上用框架还是直接操作寄存器2.1 rt_adc/rt_dac 设备框架到底能省多少事RT-Thread 的 ADC 设备框架把“打开设备、使能通道、读取值”这三件事抽象成了标准接口。读一遍rt_adc_read写一遍rt_dac_write业务层甚至不用关心你这颗芯片的 ADC 是 12 位还是 16 位也不用管寄存器地址是多少。这种抽象在功能验证阶段特别有用#include rtthread.h #include rtdevice.h #define ADC_DEV_NAME adc0 #define ADC_DEV_CHANNEL 5 rt_adc_device_t adc_dev; static void adc_sample_once(void) { rt_uint32_t raw_value; adc_dev (rt_adc_device_t)rt_device_find(ADC_DEV_NAME); if (adc_dev RT_NULL) { rt_kprintf(cannot find %s\n, ADC_DEV_NAME); return; } rt_adc_enable(adc_dev, ADC_DEV_CHANNEL); raw_value rt_adc_read(adc_dev, ADC_DEV_CHANNEL); rt_adc_disable(adc_dev, ADC_DEV_CHANNEL); rt_kprintf(raw: %u, voltage: %d.%03d V\n, raw_value, (int)(raw_value * 3300 / 4096 / 1000), (int)(raw_value * 3300 / 4096 % 1000)); }关键理解点有两个。第一rt_adc_enable不只是“打开通道”它内部会完成 ADC 外设的时钟使能、通道配置、启动转换等动作所以要放到循环外面做不要每采样一次就使能关闭一次。第二框架读到的 raw_value 只代表 ADC 数字量要换算成工程值还得结合参考电压做一次比例运算。DAC 的框架接口同样简洁#define DAC_DEV_NAME dac0 static void dac_set_voltage(void) { rt_dac_device_t dac_dev (rt_dac_device_t)rt_device_find(DAC_DEV_NAME); if (dac_dev RT_NULL) { rt_kprintf(cannot find %s\n, DAC_DEV_NAME); return; } rt_dac_enable(dac_dev, 0); rt_dac_write(dac_dev, 0, 2048); }rt_dac_write的第二个参数是通道号第三个是数字量。对 12 位 DAC 来说0-4095 对应 0V 到参考电压。这里我建议先写上2048也就是一半量程再用万用表量一下输出是否约为 1.65V以此确认通道映射和基准电压源都正确。2.2 我实际用的初始化流程框架层面对外是同一套 API但对内还是需要把引脚、时钟和采样时间配好。我的习惯是分三步。第一步查数据手册确认 ADC 通道对应的 GPIO。GD32H7 系列通常把 ADC 通道分布在 PA/PB/PC 上比如 ADC0 的通道 5 对应 PA5但这不绝对务必以你所用的具体型号引脚定义为准。第二步配置 GPIO 为模拟模式这一步容易错漏写或配成复用功能会导致采样值始终是 0 或者跳动剧烈。第三步配置 ADC 时钟。ADC 的采样时钟不是越高越好时钟太高采样电容充电时间不够低阻抗信号源可能勉强但高阻抗源就会采出明显偏小的值。用 GD32 标准库风格的代码大致是这样static void adc_gpio_init(void) { rcu_periph_clock_enable(RCU_GPIOA); gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_5); } static void adc_config(void) { rcu_periph_clock_enable(RCU_ADC0); adc_clock_config(ADC0, ADC_CLK_DIV6); adc_channel_length_config(ADC0, ADC_ROUTINE_CHANNEL, 1); adc_routine_channel_config(ADC0, 0, ADC_CHANNEL_5, ADC_SAMPLETIME_15); adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); adc_enable(ADC0); }采样时间我这里先写了ADC_SAMPLETIME_15实际使用中如果外部信号源阻抗比较大我会往上调到ADC_SAMPLETIME_239甚至更高。代价是采样速率下降但对工控里的大部分传感器信号来说完全划得来。2.3 什么时候必须绕过框架框架接口不适合所有场景。比如你要做 DMA 循环采集或者要用定时器触发 ADC 以精确对齐 PWM 中点或者要把 DAC 的输出和目标正弦表通过 DMA 连续搬运这时候rt_adc_read和rt_dac_write就有点不够用了。RT-Thread 的设备驱动模型里这几个接口是同步阻塞式不会帮你管理 DMA、触发源和中断标志。我的建议是分两层业务逻辑尽量走框架底层高频通道自己用寄存器或者直接改 BSP 驱动。如果你愿意折腾也可以在 RT-Thread 的 ADC 驱动里把 DMA 模式加进去把新的控制接口通过rt_device_control暴露出来这样既有框架的封装性又保留了底层灵活性。3. ADC 驱动实现轮询、DMA 和硬件滤波怎么配合3.1 引脚、时钟和采样周期的关系配置 ADC 时首先要想清楚输入信号的源头。温度变送器输出 4-20mA 经过 250Ω 电阻变成 1-5V这种信号源输出阻抗不高采样周期只要不是极端小都能采准。但如果你直接采集一个高内阻信号源比如几十 kΩ 的分压网络而 ADC 采样时间只有 2-3 个 ADC 时钟周期那采样保持电容还没充满电读到的值就会明显偏小。量级的估算是这样采样保持电容一般在几 pF 到十几 pF如果外部源阻抗是 10kΩ那么 RC 充放电时间常数大约是 0.1μs 量级。ADC 的采样开关导通时间必须达到几个时间常数电荷才能稳定。所以采样周期式并非越长越好但要结合源阻抗去选。实际工程里我遵守一个原则凡是有可能被分压电阻或传感器内阻拉高的输入一律把采样时间配成最大值附近先保证稳定再考虑速度。GD32H7 的 ADC 采样时间选项里有一档几十个周期的值做过程量采样完全足够。3.2 轮询模式与 DMA 模式的取舍轮询模式适合单通道、低速、不要求 CPU 实时响应场景。代码简单一条adc_software_trigger_enable后轮询结束标志读走数据就是一次采样。但轮询的问题在于CPU 被卡住等标志位RT-Thread 下其他线程会被阻塞。如果采样周期很短、频率很高轮询会占掉大量 CPU 时间。所以一旦遇到多通道连续采集或者需要和 PWM 同步我就会切到 DMA。DMA 模式下的基本思路是ADC 每一次转换完成DMA 把结果搬到内存数组里一批数据采完之后触发传输完成中断再通知 RT-Thread 的线程去处理。这样 CPU 只在这个完整周期被唤醒一次效率高很多。DMA 配置的一个关键点是数据宽度要匹配。ADC 规则数据寄存器通常是 16 位或 32 位如果你的 DMA 数据宽度配成 8 位会把一个值拆成几个字节数据全乱。我吃过这个亏查了半天最后发现是GD32_DMA_WIDTH_16BIT没配。3.3 GD32H 系列硬件滤波的使用GD32H7 的 ADC 里带了一档硬件滤波能力看参考手册里的 ADC 滤波功能它可以对转换结果做平均/低通主要是为了省掉一部分软件平均运算同时降低 CPU 压力。我在固件库的 ADC 配置里这样用adc_filter_enable(ADC0); adc_filter_config(ADC0, ADC_FILTER_ORDER_8);这里的“滤波阶数”我理解为一个滑动窗口长度。设成 8 阶相当于硬件做了 8 次结果的平均。它和软件平均的区别是硬件会在每次转换后自动滑动更新实时性比“采 N 次停下来算平均”更好内存和代码也省。但注意硬件滤波不是万能的。它对白噪声抑制有效对脉冲型干扰几乎无能为力。所以我的建议是硬件滤波负责日常平滑软件里再加一层中值判断两者配合。如果遇到 EFT 浪涌之类的瞬态干扰重点还得靠输入端的 RC 滤波和 TVS 管来解决。3.4 软件滤波C 语言实现一段实用的均值/滑动滤波与硬件滤波结合时我用过最顺手的软件滤波是滑动平均加去极值代码如下#define ADC_FILTER_BUF_LEN 16 static rt_uint32_t adc_filter_buf[ADC_FILTER_BUF_LEN]; static rt_uint8_t adc_filter_idx; rt_uint32_t adc_filter(rt_uint32_t value) { rt_uint32_t sum 0; rt_uint32_t min 0xFFFFFFFF; rt_uint32_t max 0; rt_uint8_t i; adc_filter_buf[adc_filter_idx] value; adc_filter_idx (adc_filter_idx 1) % ADC_FILTER_BUF_LEN; for (i 0; i ADC_FILTER_BUF_LEN; i) { if (adc_filter_buf[i] min) min adc_filter_buf[i]; if (adc_filter_buf[i] max) max adc_filter_buf[i]; sum adc_filter_buf[i]; } return (sum - min - max) / (ADC_FILTER_BUF_LEN - 2); }这个函数先去掉了 16 个采样里的最大最小值再对剩余 14 个求平均。这样脉冲干扰会被直接剔除不会像普通滑动平均那样被一个毛刺带偏。实测下来对 4-20mA 变送器信号原本偶尔有 ±3 个 LSB 的跳动会被压到 ±1 个 LSB 以内。缺点是多了一些比较运算但以 H759 的 CPU 算力来说这点开销完全可以忽略。4. DAC 驱动实现电压输出和定时器触发波形4.1 为什么要用 DAC 而不是 PWM经常有人问输出电压给定可以用 PWM RC 滤波为什么要上 DACPWM 加滤波的优点是便宜、驱动简单但缺点也很明显。第一PWM 滤波后纹波比较大想要纹波低就得把截止频率做得很低导致响应变慢第二PWM 的周期抖动会叠加到输出上在音频和精密模拟控制场景很致命第三PWM 输出能力一般电平转换时会有较大的毛刺。所以只要产品里真正需要“干净的模拟电压”我都会直接安排 DAC。GD32H759 的 DAC 输出是缓冲型的可以直接驱动一定大小的负载配合外部运放做电压放大或 V/I 转换。调试时先用万用表量输出再用示波器看纹波能非常直观地判断信号质量。4.2 DAC 初始化、DHR 寄存器与 rt_dac 接口GD32 的 DAC 配置逻辑里DAC 触发源可以选软件触发、定时器触发或外部触发。它的输出寄存器走一套 DHR 数据保持寄存器DHR 全称是 Data Holding RegisterDHR12R、DHR12L、DHR8R 对应不同的数据宽度和对齐方式。你用 RT-Thread 框架rt_dac_write时驱动内部会帮你处理这些寄存器的对齐但你自己写底层时一定要把握好分辨率。我实际初始化 DAC 的步骤是先使能 GPIO 时钟把 DAC 输出引脚配置为模拟模式再使能 DAC 时钟配置触发源、输出缓冲最后使能 DAC 通道。以 GD32 风格代码为例static void dac_config(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_DAC); gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_4); dac_deinit(); dac_trigger_source_config(DAC0, DAC_TRIGGER_SOFTWARE); dac_output_buffer_enable(DAC0); dac_enable(DAC0); }这里dac_output_buffer_enable就是打开输出缓冲输出能力会强不少。若你的电路后级已经有运放也可以关掉缓冲具体以驱动能力为准。4.3 用定时器触发 DAC 生成正弦波的关键代码在工控里DAC 不但要输出直流偶尔也要输出斜坡、正弦波这类规律波形。让 CPU 手动控制每个时刻的 DAC 值虽然可行但很难保证精确时间间隔因此我通常让定时器负责节奏。思路是这样预先把一个周期的正弦波离散值存进数组然后启动 DMA 循环搬运每次定时器更新事件触发 DACDAC 转换完毕产生 DMA 请求DMA 自动从数组里取下一个值写入 DHR 寄存器。整个过程中 CPU 完全不用参与DAC 输出频率由定时器的更新频率决定。关键代码框架如下#define SIN_TABLE_SIZE 256 static rt_uint16_t sin_table[SIN_TABLE_SIZE]; static void sin_table_init(void) { for (rt_uint16_t i 0; i SIN_TABLE_SIZE; i) { float v 2048.0f 2047.0f * sinf(2.0f * 3.1415926f * i / SIN_TABLE_SIZE); sin_table[i] (rt_uint16_t)v; } } static void dac_timer_dma_start(TIMER_T *timer) { dac_trigger_source_config(DAC0, DAC_TRIGGER_TIMER); dac_trigger_enable(DAC0); dma_single_data_parameter_struct dma_param {0}; dma_deinit(DMA0, DMA_CH3); dma_single_data_parameter_struct_init(dma_param); dma_param.periph_addr (uint32_t)DAC0-DHR12R0; dma_param.memory_addr (uint32_t)sin_table; dma_param.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_param.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_param.direction DMA_MEMORY_TO_PERIPH; dma_param.number SIN_TABLE_SIZE; dma_param.periph_width DMA_PERIPH_WIDTH_16BIT; dma_param.memory_width DMA_MEMORY_WIDTH_16BIT; dma_circular_mode_enable(DMA0, DMA_CH3); dma_single_data_mode_init(DMA0, DMA_CH3, dma_param); dma_channel_enable(DMA0, DMA_CH3); timer_enable(timer); }注意DMA 循环模式下必须保证内存地址连续数组大小和定时器更新频率匹配否则波形相位会跳。另外DAC 触发的真正时机是定时器更新事件所以你要确认定时器的 TRGO 输出配置正确别只是开了定时器中断结果发现波形根本不动。5. 精度上不去的三个真凶基准、噪声和 PCB5.1 从 VREF 说起基准源选择很多工程师把 ADC 的参考电压直接用 MCU 的电源引脚接上就完事这个习惯在要求不高的板子上勉强能跑但在工控上很容易翻车。因为 MCU 内核电源上会有数字翻转噪声开关噪声会直接耦合进参考电压ADC 的量化基准不稳读数自然就飘。GD32H759 这类芯片通常会有独立的 VREF 引脚或者你可以通过配置把基准源切到 VREF 通道。设计上我强烈建议用外部基准源芯片给 VREF 供电常见的像 REF3025、LM4040 这类精度和温漂都远好于直接用 LDO。只要 VREF 稳了ADC/DAC 的精度就有一大半的保障。如果硬件已经固定只能用 VDD 当基准那就必须在 PCB 布局上把 VREF 引脚和数字电路尽量隔开并加足够容量的去耦电容。软件补偿只能校准增益误差补偿不了动态噪声这一点要有清晰认知。5.2 电源噪声与时钟抖动的影响ADC 是模拟电路和数字电路混合的边界。采样保持开关打开时采样电容从外部输入取样同时 VREF 要在极短时间内给内部电容网络充电这个过程一旦叠上电源噪声直接结果就是转换结果的低位乱跳。时钟抖动同样会伤害 ADC。转换时钟每个周期的时间抖动会导致比较器的比较时刻不稳定尤其在高频输入信号时时间轴上的抖动会转换成幅度上的噪声表现为采样值在某一两个 LSB 之间跳动。这种问题靠调采样时间很难消除得从时钟源、PLL 配置和 PCB 走线去治。我一般会看 ADC 时钟是从哪个 PLL 分出来的如果 PLL 锁相环的参数配得不好输出频谱里可能带杂散。另一个常见做法是稍微降低 ADC 时钟频率比如配置成 6 分频、8 分频换来的是更好的稳定度。对工控应用来说采样率低一点通常没关系稳定才是第一位的。5.3 PCB 布局的三个要点结合我自己的踩坑经验PCB 布局上最重要的三个要点可以这样概括。第一ADC/DAC 相关的模拟地要和数字地做规划但不一定非得暴力分割。我的经验是电源地、MCU 地、模拟电路地尽量在一个完整的地平面上再通过单点或窄桥方式连接到系统地避免地平面形成天线。模拟采样回路要短、要粗尽量避开开关电源的电感下方。第二VREF、AVDD 引脚附近要放高质量去耦电容我习惯用电容组一个 10μF 钽电容或 MLCC一个 0.1μF 小电容紧贴引脚放置。去耦电容的地过孔要就近打不能绕很远再接地否则效果会大打折扣。第三ADC 输入端前面要留了对地电容和串联电阻的位置。这个 RC 电阻电容除了抗干扰还给采样保持电容提供足够的瞬时充电电流来源。RC 截止频率要结合信号带宽选择不能一味加大电容不然信号相位延迟会变大。需要说明的是根据你手里的应用场景这些数值最好实际调试确定。5.4 一个实际的数据漂移排查过程我之前遇到过一个现象ADC 常温下稳定跑半小时后读数逐渐往下漂了十几个 LSB。一开始怀疑是基准芯片温漂但用万用表量基准电压发现只有 1mV 变化不像是主因。后来把示波器探头接到 ADC 输入引脚发现信号本身还在缓慢上升原来板子上传感器激励电源用的是普通三端稳压发热后输出电压下降导致传感器输出整个跟着漂。这个案例给了一个很重要的排查思路当 ADC 数据漂移时不能只盯着 MCU 侧要从“信号源头 → 传输链路 → 调理电路 → MCU 引脚 → 参考电压 → 软件换算”整条链路逐级排查。很多看起来像 ADC 的问题其实根本不是 ADC 的问题。排查链路我一般这样走把 ADC 输入引脚短接到 GND读到的值应当在接近 0 的很小范围内如果不是说明后端有偏置或基准问题。把 ADC 输入引脚接到 VREF读到的值应当接近满量程以此验证参考电压和通道增益。用稳定的可调电源分别给 10%、50%、90% 量程电压记录 ADC 读数计算实际增益和偏置误差再做软件校正。将信号源断开直接把引脚悬空或通过电阻接地观察底噪以此判断 MCU ADC 自身噪声。这套方法我在每一块工控板上都会做一遍能快速定位问题出在软件、外围电路还是电源。6. 调试验证如何证明这套驱动真的能用6.1 用波形和分压验证线性度ADC/DAC 驱动写完不能只在终端打印几个数据就说“通了”。我会用可调直流稳压电源加一个精密电阻分压网络给 ADC 输入 0V、0.5V、1V、1.5V、2V、2.5V、3.0V、3.3V 这组电压把实际读数和理论值列成表。通过对比算出线性度误差。如果线性度不好先查外部 RC 滤波对信号的衰减再查参考电压是否稳定最后查 ADC 内部校准系数。DAC 的线性度验证反之往 DAC 里写 0、512、1024、2048、3072、4095用万用表量输出电压。如果输出和理论值偏差大了可能是 DAC 缓冲器负载太重也可能基准电压不准。这里我习惯把负载断开再量一次对比带载和空载的电压差以此判断驱动能力。6.2 频谱和统计方法看噪声示波器看 ADC 输入波形时能看到的是宏观噪声很难看出具体频率成分。更好用的办法是把采样数据通过串口传到上位机用 Python 或 MATLAB 画功率谱看信号里有没有 50Hz 工频、开关电源开关频率以及这些频率的谐波。如果频谱上有一个明显的窄带尖峰比如 300kHz 左右多半是板载 DC-DC 的开关频率串进来了。这个噪声靠软件滤波很难完全抑制因为频带太窄最终还是要从布局或屏蔽下手。如果是均匀分布的底噪软件滤波效果会好很多。用统计方法看也很直观连续采 1000 个点计算标准差。如果标准差小于 1-2 个 LSB说明这条采集链路噪声水平可以接受如果标准差十几个 LSB就要继续排查。这个方法尤其适合做产线质检时给每块板子定“噪声基线”。6.3 长稳测试最后一关是长时间稳定性测试。我会让板子连续运行 24 小时固定采集一个稳定的模拟信号每隔 5 分钟记录一次平均值。重点关注两个指标一是均值是否随时间缓慢漂移二是瞬时值是否有间歇性跳变。均值漂移很大可能是温漂间歇性跳变则要怀疑干扰或程序里缓冲区越界。实际遇到的情况里间歇性跳变往往最隐蔽它可能几小时才出现一次但一旦跳变发生在 PID 闭环里就可能被误判成真实负载突变引起输出振荡。长稳测试跑完后如果数据仍然平稳我才会把这段 ADC/DAC 驱动代码固化成正式版本再进下一个功能模块。最后分享一个我自己的习惯无论用 RT-Thread 框架还是直接操作寄存器我都会在 ADC/DAC 驱动里保留一个调试用的 shell 命令比如adc_get、dac_set 2048。后续在项目现场调试时不需要重新烧程序就能读到模拟量、输出固定电压排查问题效率会高非常多。这个习惯帮我省了很多来回烧固件的时间你可以试试。
返回列表