ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

GD32H759在RT-Thread下的ADC/DAC驱动设计与工控应用

2026/9/20 16:44:21 拓冰建站 浏览量
GD32H759在RT-Thread下的ADC/DAC驱动设计与工控应用 1. 项目背景与整体设计思路1.1 为什么工控场景下ADC/DAC驱动值得单独拎出来讲在工控板卡开发里MCU的GPIO翻转、串口收发这些外设调通一次基本就能稳定跑很久。但ADC和DAC不一样它们直接跟模拟世界打交道板子上的电源纹波、地平面噪声、走线耦合、参考电压抖动任何一个环节出问题采样值就会飘。我在做GD32H759这块板子的数据采集模块时最开始用轮询方式读ADC发现采出来的值跳动能有±15个LSB后来换成DMA加定时器触发配合硬件过采样才把波动压到±2个LSB以内。这篇内容主要围绕GD32H759这颗Cortex-M7内核的高性能MCU在RT-Thread操作系统下如何把ADC和DAC驱动跑通、跑稳。适合已经上手过RT-Thread基础外设驱动、正在做数据采集或模拟量输出类工控项目的开发者参考。如果你之前只用过STM32的HAL库转到GD32的固件库加RT-Thread的设备驱动框架中间有不少细节需要重新捋一遍。GD32H759的ADC是12位逐次逼近型最高采样率可以到5.3MSPS左右支持差分输入、硬件过采样、模拟看门狗等功能。DAC是12位电压输出型带输出缓冲可以配置成噪声波或三角波发生器。这些特性在工控场景里很实用比如电机电流采样需要高速ADC配合定时器同步触发而模拟量输出控制阀门或变频器则需要DAC稳定输出0-10V或4-20mA信号。1.2 驱动方案选型RT-Thread设备框架 vs 裸机寄存器操作RT-Thread提供了ADC设备驱动框架通过rt_adc_enable、rt_adc_read这些标准接口就能读值。但实际工控项目里我建议不要完全依赖这套上层接口原因有几个第一标准接口每次读都要走一遍设备查找和参数检查在1kHz以上的采样率下开销不可忽略第二硬件过采样、差分输入、模拟看门狗这些高级功能标准接口没有直接暴露第三DMA搬运和定时器触发这种组合拳需要直接操作寄存器才能配得灵活。我的做法是底层用GD32的固件库直接配置ADC和DAC的寄存器把DMA通道和定时器触发源接好然后在RT-Thread里注册一个字符设备或者直接用消息队列把数据传给应用线程。这样既保留了RT-Thread的线程调度和同步机制又不会被上层框架的限制卡住。DAC这边相对简单一些因为输出速率通常不高用RT-Thread的DAC设备框架或者直接写寄存器都行。但要注意DAC的输出建立时间和负载效应如果后面接的是运放或者隔离变送器输出阻抗和容性负载会影响稳定性。1.3 整体数据流设计整个ADC/DAC驱动的数据流是这样的定时器产生周期性触发信号ADC在触发沿启动转换转换完成后DMA把数据搬到内存缓冲区缓冲区半满或全满时产生中断在中断里给RT-Thread的信号量或消息队列发通知应用线程拿到数据后做滤波、标定、打包再通过串口或以太网上传。DAC这边应用线程把要输出的值写入DMA缓冲区定时器触发DAC转换DMA自动搬运下一个值实现任意波形的连续输出。这个架构的关键在于触发源和DMA的配合。定时器的触发频率决定了采样率DMA的搬运宽度和缓冲区大小决定了能连续采集多长时间而不丢数据。我一般会把缓冲区设成两倍于一次处理所需的数据量用双缓冲模式这样在处理当前缓冲区的数据时下一个缓冲区已经在后台填充了。2. ADC驱动核心细节与实操要点2.1 GD32H759 ADC的时钟与参考电压配置GD32H759的ADC时钟来源是APB2总线时钟经过一个分频器最高不能超过60MHz。我一般会把APB2设到120MHz然后ADC分频系数选2得到60MHz的ADC时钟。采样率计算公式是采样率 ADC时钟 / (采样周期 转换周期)。12位转换周期固定是12个ADC时钟采样周期可以设成1.5到239.5个时钟周期不等。参考电压这块要特别注意。GD32H759的VREF可以外部提供也可以用内部的VREFBUF。工控场景下我强烈建议用外部精密基准源比如REF5025或者ADR4525温漂能控制在3ppm/°C以内。如果用MCU的VDDA做参考开关电源的纹波会直接调制到采样值上12位ADC的有效位数可能只剩9位不到。注意VREF引脚旁边一定要放一个1μF的陶瓷电容和一个100nF的高频电容且尽量靠近引脚。我踩过的坑是只放了100nF结果采样值在低频段有周期性波动后来加了1μF才压下去。2.2 硬件过采样与差分输入的配置方法GD32H759的ADC支持硬件过采样可以配置2x到256x的过采样率还能选择右移位数。这个功能在工控里非常实用比如电机电流采样PWM开关噪声很大硬件过采样相当于在ADC内部做了多次采样求平均比软件滤波省CPU且效果更好。配置过采样的寄存器是ADC_OVSAMPCTL需要设置OVSEN使能位、OVSR过采样率、OVSS右移位数。我一般用16x过采样右移4位这样等效于把12位ADC变成16位分辨率但实际有效位数大概能到13.5位左右。右移位数不能超过过采样率的log2值否则会丢失有效数据。差分输入这边GD32H759的ADC支持全差分和伪差分模式。全差分模式下两个输入引脚分别接信号的正负端共模电压要设在VREF/2附近。伪差分模式是一个引脚接信号另一个接固定参考。工控现场的传感器很多是差分输出的比如应变桥式传感器用全差分模式能有效抑制共模干扰。2.3 定时器触发与DMA搬运的联合配置定时器触发ADC转换是工控采样的标准做法。我一般用高级定时器TIMER0或通用定时器TIMER2配置成PWM模式或者输出比较模式在更新事件时产生触发信号。触发源要选对GD32H759的ADC支持多种触发源包括定时器0的CH0、CH1、CH2、CH3定时器1的CH0定时器2的TRGO等。DMA配置这边要把ADC的DMA请求使能设置成循环模式数据宽度选半字16位因为ADC数据寄存器是16位的。DMA的源地址是ADC的规则数据寄存器目标地址是内存缓冲区。缓冲区大小我一般设成256或512个半字配合双缓冲模式半满和全满各产生一次中断。// ADCDMA定时器触发的关键配置片段 void adc_dma_timer_init(void) { // 定时器配置1kHz触发频率 timer_parameter_struct timer_initpara; rcu_periph_clock_enable(RCU_TIMER2); timer_deinit(TIMER2); timer_initpara.prescaler 12000 - 1; // 120MHz/12000 10kHz timer_initpara.period 10 - 1; // 10kHz/10 1kHz timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_init(TIMER2, timer_initpara); // 配置TRGO输出更新事件 timer_master_output_trigger_source_select(TIMER2, TIMER_TRI_OUT_SRC_UPDATE); timer_enable(TIMER2); // ADC配置 rcu_periph_clock_enable(RCU_ADC0); adc_deinit(ADC0); adc_clock_config(ADC_ADCCK_PCLK2_DIV2); adc_resolution_config(ADC0, ADC_RESOLUTION_12B); adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); adc_channel_length_config(ADC0, ADC_ROUTINE_CHANNEL, 1); adc_routine_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_56); // 外部触发配置 adc_external_trigger_source_config(ADC0, ADC_ROUTINE_CHANNEL, ADC_EXTTRIG_ROUTINE_T0_CH0); adc_external_trigger_config(ADC0, ADC_ROUTINE_CHANNEL, ENABLE); // DMA配置 rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH0); dma_init_struct.direction DMA_PERIPHERAL_TO_MEMORY; dma_init_struct.memory_addr (uint32_t)adc_buffer; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init_struct.number ADC_BUFFER_SIZE; dma_init_struct.periph_addr (uint32_t)ADC_RDATA(ADC0); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH0, dma_init_struct); dma_circulation_enable(DMA0, DMA_CH0); dma_memory_to_memory_disable(DMA0, DMA_CH0); dma_transfer_direction_config(DMA0, DMA_CH0, DMA_PERIPHERAL_TO_MEMORY); dma_channel_enable(DMA0, DMA_CH0); adc_dma_mode_enable(ADC0); adc_enable(ADC0); adc_calibration_enable(ADC0); }这段代码里有个细节adc_calibration_enable必须在ADC使能之后、开始转换之前调用否则校准不生效。我一开始把校准放在使能之前结果采样值一直偏大查了半天才发现是校准没跑。2.4 采样数据的软件滤波与标定硬件过采样之后软件这边我还会加一层滑动平均或者中值滤波。滑动平均适合抑制随机噪声中值滤波适合剔除脉冲干扰。工控现场两种干扰都有所以我一般用“中值滑动平均”的组合先取5个点做中值再对中值结果做8点滑动平均。标定这边两点标定法最实用。给ADC输入两个已知电压比如0.5V和2.5V记录对应的采样值算出斜率和截距。工控里经常用4-20mA电流环需要在ADC前端加一个精密电阻把电流转成电压比如250Ω电阻把4-20mA转成1-5V。这时候标定点就选1V和5V对应的采样值。实操心得标定的时候不要只标一次就完事最好在常温下标定后再放到高低温箱里跑一遍记录温漂曲线。如果温漂超过0.1%/°C就要考虑加温度补偿或者换低温漂的基准和电阻。3. DAC驱动核心细节与实操要点3.1 DAC输出缓冲与负载匹配GD32H759的DAC输出缓冲可以使能或禁用。使能缓冲时输出阻抗低驱动能力强但输出范围会受限大概在0.2V到VDDA-0.2V之间。禁用缓冲时输出范围能到轨到轨但输出阻抗高带负载能力弱。工控场景下如果DAC后面直接接运放做电压跟随我建议禁用缓冲让运放来处理驱动问题如果直接驱动小负载就使能缓冲。负载匹配这块DAC输出端一定要加RC低通滤波截止频率根据信号带宽来定。比如输出的是4-20mA对应的控制电压带宽只有几十HzRC可以选1kΩ10μF截止频率约16Hz能有效滤掉高频噪声。但要注意电容的等效串联电阻和温度特性X7R或X5R的陶瓷电容在温度变化时容值会变最好用C0G或薄膜电容。3.2 定时器触发DAC与DMA波形输出DAC要输出连续波形必须用定时器触发加DMA搬运。GD32H759的DAC支持多种触发源包括定时器0的TRGO、定时器1的TRGO、定时器2的TRGO等。我一般用TIMER1的TRGO因为TIMER1是通用定时器配置灵活。DMA这边要把DAC的DMA请求使能设置成循环模式数据宽度选半字。DMA的源地址是波形数据数组目标地址是DAC的数据保持寄存器。每次定时器触发DAC就从DHR寄存器取一个新值转换输出。// DACDMA定时器触发输出正弦波的关键配置 #define WAVE_POINTS 256 uint16_t sine_wave[WAVE_POINTS]; void dac_wave_init(void) { // 生成正弦波表0-4095对应0-3.3V for (int i 0; i WAVE_POINTS; i) { sine_wave[i] (uint16_t)(2048 2047 * sin(2 * 3.1415926 * i / WAVE_POINTS)); } // 定时器配置输出10kHz正弦波 timer_parameter_struct timer_initpara; rcu_periph_clock_enable(RCU_TIMER1); timer_deinit(TIMER1); timer_initpara.prescaler 120 - 1; // 120MHz/120 1MHz timer_initpara.period 100 - 1; // 1MHz/100 10kHz timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_init(TIMER1, timer_initpara); timer_master_output_trigger_source_select(TIMER1, TIMER_TRI_OUT_SRC_UPDATE); timer_enable(TIMER1); // DAC配置 rcu_periph_clock_enable(RCU_DAC); dac_deinit(); dac_trigger_source_config(DAC_OUT0, DAC_TRIGGER_T1_TRGO); dac_trigger_enable(DAC_OUT0); dac_wave_mode_config(DAC_OUT0, DAC_WAVE_DISABLE); dac_output_buffer_enable(DAC_OUT0); dac_enable(DAC_OUT0); // DMA配置 rcu_periph_clock_enable(RCU_DMA1); dma_deinit(DMA1, DMA_CH0); dma_init_struct.direction DMA_MEMORY_TO_PERIPHERAL; dma_init_struct.memory_addr (uint32_t)sine_wave; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init_struct.number WAVE_POINTS; dma_init_struct.periph_addr (uint32_t)DAC_DHR12R0; dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA1, DMA_CH0, dma_init_struct); dma_circulation_enable(DMA1, DMA_CH0); dma_channel_enable(DMA1, DMA_CH0); dac_dma_enable(DAC_OUT0); }这段代码里正弦波表用sin函数生成但要注意sin返回的是浮点数乘完2047后要强制转成uint16_t。另外DAC的数据对齐方式要跟DMA的搬运宽度匹配12位右对齐时DHR12R0寄存器的低12位有效DMA搬16位数据时高4位会被忽略所以波形表的值不能超过4095。3.3 DAC输出建立时间与更新速率DAC的输出建立时间是指从DHR寄存器写入新值到输出稳定到目标值±0.5LSB所需的时间。GD32H759的DAC建立时间典型值是1μs左右所以更新速率最高可以到1MSPS。但实际工控场景下DAC后面通常接RC滤波和运放运放的压摆率会限制实际建立时间。比如用OPA2277做输出缓冲压摆率是2.3V/μs输出3V跳变需要1.3μs加上RC滤波的充电时间整体建立时间可能到10μs以上。所以DAC的更新速率不能只看DAC本身要把整个模拟链路的建立时间算进去。我一般会留3倍余量比如链路建立时间10μs更新周期就设成30μs以上对应33kHz以下的更新率。注意DAC输出端如果接的是隔离变送器变送器内部通常有采样保持和调制解调建立时间会更长。我遇到过用某品牌隔离变送器DAC更新率超过1kHz后输出就开始畸变后来降到500Hz才正常。3.4 DAC输出值的标定与校准DAC的标定比ADC简单因为输出是确定的。给DHR寄存器写0测输出电压再写4095测输出电压算出实际斜率和截距。工控里经常需要输出0-10V或4-20mADAC的0-3.3V输出需要经过运放放大。放大电路的增益电阻精度直接影响输出精度我一般用0.1%精度的电阻温漂25ppm/°C以内的。校准的时候不要只校两个点最好在0%、25%、50%、75%、100%五个点各测一次看线性度。如果非线性误差超过0.1%就要考虑DAC本身的积分非线性指标或者放大电路的线性度问题。4. RT-Thread环境下的驱动集成与线程同步4.1 在RT-Thread中注册ADC/DAC设备RT-Thread的设备驱动框架要求先注册设备再通过rt_device_find查找。对于ADCRT-Thread有标准的rt_adc_device结构但前面说了我建议底层自己管只在上层用RT-Thread的同步机制。具体做法是在rt_hw_adc_init里完成GD32的ADC寄存器配置和DMA初始化然后创建一个消息队列或信号量在DMA中断里释放信号量应用线程等待信号量后从缓冲区取数据。如果一定要用RT-Thread的ADC框架需要实现rt_adc_ops结构里的enabled、convert、control这几个回调。convert回调里读ADC数据寄存器返回采样值。但这种方式每次读都要进一次回调效率不高适合低速采样场景。DAC这边RT-Thread有rt_dac_device框架实现rt_dac_ops里的enabled和set_value回调即可。set_value回调里把值写入DHR寄存器。但同样高速波形输出还是要自己配DMA和定时器。4.2 DMA中断与线程间通信DMA半满和全满中断里我一般只做两件事清除中断标志释放信号量。数据处理放在应用线程里做避免在中断里做浮点运算或内存拷贝。信号量用rt_sem_release释放应用线程用rt_sem_take等待超时时间设成采样周期的两倍防止死等。// DMA中断服务程序 void DMA0_Channel0_IRQHandler(void) { if (dma_interrupt_flag_get(DMA0, DMA_CH0, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF); rt_sem_release(adc_sem); } if (dma_interrupt_flag_get(DMA0, DMA_CH0, DMA_INT_FLAG_HTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_HTF); rt_sem_release(adc_sem); } } // 应用线程 void adc_thread_entry(void *parameter) { while (1) { if (rt_sem_take(adc_sem, rt_tick_from_millisecond(10)) RT_EOK) { // 处理adc_buffer里的数据 process_adc_data(adc_buffer, ADC_BUFFER_SIZE); } } }这里有个坑DMA中断标志清除要在释放信号量之前否则如果信号量释放后线程立刻被调度中断标志还没清会再次进中断。我一开始顺序反了导致中断频繁触发CPU占用率飙到80%。4.3 采样线程与输出线程的优先级安排RT-Thread的线程优先级是数值越小优先级越高。ADC采样线程我一般设成优先级10DAC输出线程设成优先级12数据处理和通信线程设成优先级15。这样采样线程能及时响应DMA中断DAC输出线程保证波形连续数据处理线程在后台慢慢算。如果采样率很高比如100kHz以上采样线程可能来不及处理所有数据。这时候要么提高采样线程优先级要么用双缓冲加零拷贝的方式让采样线程只负责切换缓冲区数据处理交给更低优先级的线程。实操心得RT-Thread的线程栈大小要留够。ADC数据处理如果用了浮点运算和printf栈至少给2KB。我一开始给1KB结果跑一段时间就hard fault查了半天是栈溢出。5. 常见问题与排查技巧实录5.1 ADC采样值跳动大或始终为0采样值跳动大先查参考电压。用示波器看VREF引脚纹波超过1mV就会影响12位ADC。如果VREF是从VDDA分压来的大概率是电源纹波问题换外部基准源。再查模拟输入端的RC滤波截止频率太高会让高频噪声混进来太低会让信号建立时间不够。我一般用1kΩ100nF截止频率约1.6kHz对大多数工控信号够用。采样值始终为0先查ADC使能位和校准位。GD32H759的ADC使能后需要等稳定时间大概1μs左右然后才能校准。校准需要大概14个ADC时钟周期。如果校准没完成就开始转换读出来的就是0。再查DMA配置如果DMA没使能或者传输方向配反了数据也搬不到缓冲区。5.2 DAC输出无变化或波形畸变DAC输出无变化先查DAC使能位和触发源。如果触发源选错了DAC不会更新。再查DMA配置如果DMA没使能或者循环模式没开波形只输出一次就停了。还要查DHR寄存器的数据对齐方式12位右对齐时写超过4095的值会被截断。波形畸变先查定时器触发频率和DAC建立时间是否匹配。触发太快DAC还没建立就更新下一个值输出就会失真。再查RC滤波的截止频率如果截止频率低于信号频率波形会被严重衰减。我遇到过用10kΩ100nF做滤波截止频率160Hz输出1kHz正弦波时幅度只剩一半。5.3 DMA传输丢数据或中断不触发DMA丢数据先查DMA缓冲区大小和采样率是否匹配。缓冲区太小DMA搬完一圈后如果CPU还没处理完新数据就会覆盖旧数据。我一般把缓冲区设成一次处理数据量的4倍以上。再查DMA优先级如果ADC的DMA和别的外设共用DMA控制器优先级低的会被抢占。中断不触发先查中断使能位和NVIC配置。GD32H759的DMA中断需要在DMA控制器和NVIC里都使能。再查中断标志清除时机如果清除太早中断可能丢失清除太晚会重复进中断。我一般是在中断服务程序开头清除标志然后处理数据。5.4 常见问题速查表现象可能原因排查方法解决方案ADC采样值跳动大参考电压纹波大示波器测VREF换外部基准源加滤波电容ADC采样值始终为0校准未完成查校准寄存器使能后延时再校准DAC输出无变化触发源配置错误查触发源寄存器选对定时器TRGODAC波形畸变建立时间不够算触发周期降低触发频率DMA丢数据缓冲区太小查缓冲区大小增大缓冲区用双缓冲DMA中断不触发NVIC未使能查NVIC寄存器使能对应中断通道采样线程栈溢出栈太小查hard fault信息增大线程栈中断频繁触发标志清除顺序错查中断服务程序先清标志再释放信号量5.5 几个容易忽略的硬件细节ADC输入端一定要加钳位保护。工控现场传感器接线错误或者浪涌可能把高于VDDA的电压灌进ADC引脚烧毁内部ESD二极管。我一般用BAV99双二极管做钳位再加一个100Ω限流电阻。DAC输出端如果接长线要加RC滤波和TVS管。长线容易耦合干扰TVS管能吸收浪涌。RC滤波的电阻不要太大否则输出阻抗高容易受后级负载影响。地平面分割要合理。模拟地和数字地单点连接连接点选在ADC或DAC的AGND引脚附近。如果地平面分割不好数字噪声会通过地平面耦合到模拟部分采样值上会看到跟数字信号同步的尖峰。最后分享一个小技巧调试ADC的时候先把采样率降到很低比如10Hz用万用表测输入电压跟ADC读数对比确认标定准确。然后再逐步提高采样率观察噪声变化。这样能把标定问题和噪声问题分开排查效率高很多。