资讯详情

资讯详情

GD32H759 ADC/DAC实战:基于RT-Thread的模拟外设驱动与设计要点

前两篇把GD32H759的工程环境、时钟树、串口这些外围跑通之后这一篇终于要啃工控里最实在的骨头了模拟量采集和输出。GD32H759带着一颗Cortex-M7跑到600MHz的主频跑RT-Thread本身就是很爽的一件事但真正到了现场决定一个系统能不能用的往往不是主频而是ADC采得准不准、DAC输出稳不稳。这篇文章我会从RT-Thread的设备驱动框架讲起一路拆到GD32H759片内ADC/DAC的寄存器级操作中间穿插DMA搬运、M7的Cache一致性、硬件过采样配置以及PCB布局上必须避开的坑。无论你是刚从STM32转过来、还是第一次在RT-Thread里碰模拟外设照着做就能少走不少弯路。1. GD32H759的ADC/DAC外设先搞清楚能干什么1.1 三种典型的模拟量采集场景在工控项目里ADC/DAC永远不会单独存在。你至少要面对下面这几类情况而且对驱动的要求完全不同第一类是慢速传感器巡检。比如温湿度、压力、液位这类信号变化很慢几十毫秒采一次完全够用。这种场景用RT-Thread标准ADC设备框架最舒服应用层调rt_adc_read把那一路电压读回来代码简单、逻辑清晰不需要关心底层寄存器。第二类是高速连续采样。比如电机电流波形分析、电网电压暂降记录这类应用采样率动不动就是几百kSPS甚至上Msps。这时候必须用到多通道扫描DMA搬运让硬件自己把数据流进内存CPU只在缓冲区满了之后做一次处理。第三类是高精度微弱信号测量。4~20mA电流环、称重传感器、热电偶冷端补偿这类场景信号幅度小、噪声大单纯靠12位ADC的原始分辨率不够用。这时候就要用GD32H759的硬件过采样功能把有效位数往上抬同时在软件层做滤波。这篇文章就是围绕这三类场景展开的。你会发现同一个片内外设在不同需求下驱动的写法和配置思路差别非常大。1.2 片内模拟外设的硬实力GD32H759的ADC是12位逐次逼近型SAR不是那些低端MCU上常见的中低精度外设。片上集成了多个ADC模块可以独立工作也可以配合DMA完成多通道连续扫描。最重要的是它支持硬件过采样能把采样结果扩展到16位在规格书里这是拿得出手的卖点实际使用中对于提高信噪比也确实有效。DAC这边是12位分辨率的电压输出型支持双通道独立或同步输出。让我觉得比较实用的是它内部有一套数据保持寄存器DHR配合定时器触发和DMA可以不用CPU干预就输出连续的波形——正弦波、三角波、阶梯波都能做。引脚复用上也够灵活。ADC输入通道分布在好几个GPIO端口上DAC的输出引脚也有多个映射可选。我实际用下来感觉引脚分配比STM32F4那种固定映射要宽松一些画PCB的时候自由度更高。举个例子我用GD32H759做过一个电机驱动器的电流环采样。三相电流通过采样电阻转成电压信号ADC需要同时采集三路电流和一路母线电压。如果用单通道轮询的方式采CPU会被转换等待完全占住而且四路信号无法保证同步。后来改成ADC多通道扫描加DMA搬运CPU只负责在每个PWM周期结束后读取最新一批电流数据做FOC计算整个系统跑起来非常顺。这就是选对方案和选错方案的差别。2. RT-Thread设备驱动框架从“管”到“用”的完整链路2.1 驱动是怎么一步步注册进系统的很多初学者在用RT-Thread的ADC设备时只知道调一下rt_adc_read就能拿到数据但不知道背后发生了什么。一旦遇到“设备找不到”或者“读了但永远是0”就不知道怎么排查了。RT-Thread的设备驱动框架本质上是一套面向对象的管理体系。拿ADC来说BSP驱动层会定义一个struct rt_adc_device结构体里面装着这个ADC设备的操作函数集struct rt_adc_ops。操作函数集里最关键的两个成员是convert和enabled一个负责转换一个通道并返回原始值另一个负责使能或关闭某个通道。驱动注册的过程可以这样理解BSP里先调rt_hw_adc_init()在函数里完成GPIO引脚配置、ADC外设时钟使能、采样通道初始化然后把这个设备挂到系统中比如调rt_device_register(device, adc0, RT_DEVICE_FLAG_RDONLY)。这一步做完整个系统中就有了一个叫“adc0”的设备节点。应用层拿到这个设备的句柄之后调用rt_device_find(adc0)找到它然后通过rt_device_open打开再调rt_device_control做控制最后调rt_device_read读数据。设备框架层会分发这些调用到对应的ops函数上。也就是说你在应用层敲的每一行API最终都会落到BSP驱动里你写的那个函数上。这套框架的好处是应用层代码与硬件解耦。今天用的是GD32H759明天换成别的芯片只要BSP驱动实现了同样的ops接口应用层一行都不用改。2.2 应用层API三分钟把采集跑通先说ADC的使用方式。在RT-Thread里找到一个ADC设备并且采集一路数据的标准代码是这样的#include rtthread.h #include rtdevice.h #define ADC_DEV_NAME adc0 #define ADC_DEV_CHANNEL 5 #define ADC_REF_VOL 3300 // 参考电压单位mV static int adc_sample_demo(void) { rt_adc_device_t adc_dev; rt_uint32_t value, vol; adc_dev (rt_adc_device_t)rt_device_find(ADC_DEV_NAME); if (adc_dev RT_NULL) { rt_kprintf(find %s failed!\n, ADC_DEV_NAME); return -RT_ERROR; } rt_adc_enable(adc_dev, ADC_DEV_CHANNEL); value rt_adc_read(adc_dev, ADC_DEV_CHANNEL); vol value * ADC_REF_VOL / 4096; // 12位ADC满量程4096 rt_kprintf(adc channel %d raw %d, voltage %d mV\n, ADC_DEV_CHANNEL, value, vol); rt_adc_disable(adc_dev, ADC_DEV_CHANNEL); return RT_EOK; } INIT_APP_EXPORT(adc_sample_demo);注意我在这里直接用了vol value * 3300 / 4096来换电压没有去调什么花哨的helper函数。原因是不同RT-Thread版本对“参考电压”的API实现不一样有的版本有rt_adc_read_vcc有的版本没有自己写一个换算反而最稳这套代码拿到任何BSP上都能跑。DAC的使用方式类似只是把读写方向反过来#define DAC_DEV_NAME dac0 #define DAC_DEV_CHANNEL 0 rt_dac_device_t dac_dev (rt_dac_device_t)rt_device_find(DAC_DEV_NAME); rt_dac_enable(dac_dev, DAC_DEV_CHANNEL); rt_dac_write(dac_dev, DAC_DEV_CHANNEL, 2048); // 输出中间电压 rt_dac_disable(dac_dev, DAC_DEV_CHANNEL);DAC的满量程也是4096rt_dac_write写入的数字值会经过DAC转换后从引脚输出。写入2048就是大约半量程电压比如3.3V参考电压下大约是1.65V。这里有一个很容易踩的坑在做完rt_dac_enable之后立刻调rt_dac_write有些芯片的DAC输出会有短暂毛刺。如果你对输出时序敏感建议在enable之后加一个微秒级延时或者把初始值在enable之前先写到DHR寄存器里。这个问题在第5章讲寄存器时会详细说。3. 实战一ADC单通道采集与两种滤波方案3.1 采样周期怎么算参考电压怎么选先解决“能不能采准”的问题。ADC采样看似简单其实里面最容易被忽略的就是采样时间和源阻抗的匹配。逐次逼近型ADC内部有一个采样保持电容转换之前先要给这个电容充电。如果信号源的输出阻抗太高而采样时间又太短电容充不满采到的电压就会偏低表现为“小信号压得很大信号还凑合”的非线性误差。GD32H759的采样时间是可以配置的。我在BSP里常用的一种配置方法是先用adc_channel_sample_time_config把采样时间设得充裕一些比如配置成60多个ADC时钟周期再去跑转换。具体是否需要这么长时间取决于你的传感器输出阻抗。假设ADC时钟配置为30MHz采样时间设为55.5个时钟周期转换阶段固定消耗12.5个时钟周期那么单次转换总耗时大约是(55.5 12.5) / 30MHz ≈ 2.27微秒换算一下就是大约440kSPS的吞吐率。这个速度对于工控里的慢速传感器巡检绰绰有余但如果你把ADC时钟继续往上提就要注意是否超出手册里允许的范围。再说参考电压。ADC的转换结果本质上是输入电压与参考电压的比值。如果你的参考电压本身不稳定、有纹波那么无论采样时间配置得多完美结果都会跟着抖动。所以工控板上如果要求ADC精度我会强烈建议VREF引脚不要直接接3.3V电源而是接一个低噪声基准芯片比如REF3033这种3.3V输出的或者针对5V系统用ADR4550。内部参考也能用但温漂和噪声指标跟外部基准芯片完全不是一个量级。3.2 GD32H7的硬件过采样其实是一种“免费”的精度提升GD32H759的ADC硬件过采样是我觉得这芯片特别值得写的一点。传统上如果嫌弃12位分辨率不够很多人会选择外挂一颗16位ADC芯片成本高、电路复杂。而GD32H7直接在片内集成了过采样模块采样率扩展4倍就能提升1位有效分辨率扩展256倍能提升4位从12位直接拉到16位分辨率。原理上这靠的是一种统计学效应信号是确定性的多次采样后累加再移位信号幅度线性增长而噪声是随机分布的累加后只按平方根速度增长。所以信噪比会随着过采样倍率的提升而变好。在BSP驱动里配置硬件过采样的流程大致是使能ADC时钟、先把过采样倍率配置好、使能过采样模式然后正常启动转换。转换完成后从结果寄存器读到的就已经是16位数据了读取逻辑和普通12位完全相同只是满量程从4096变成了65536。这里有一个关键点要提醒过采样倍率越高等效采样率下降越明显。比如ADC实际转换100kSPS过采样16倍之后有效的独立数据率只剩下大约6.25kSPS。所以它适合温漂、低频振动、称重这类信号变化慢的场景不适合电机电流环那种需要高速采样的场合。我实测下来在称重传感器上把过采样开到64倍数据的稳定性肉眼可见地变好波动幅度能减少一个数量级。3.3 软件滤波滑动平均的实现与使用边界硬件过采样只能解决ADC内部噪声和量化噪声解决不了工控现场的脉冲干扰。比如变频器启动瞬间的辐射干扰会在ADC结果里跳出一个明显偏离真实值的尖峰。对付这种问题在软件侧做一个滤波是标准做法。我在工控代码里最常用的滤波函数是滑动平均因为它实现简单、实时性好而且对高斯白噪声的抑制效果不错#define FILTER_LEN 8 static rt_uint32_t filter_buf[FILTER_LEN]; static rt_uint8_t filter_index; rt_uint32_t adc_moving_average(rt_uint32_t new_value) { rt_uint32_t sum 0; rt_uint8_t i; filter_buf[filter_index] new_value; filter_index (filter_index 1) % FILTER_LEN; for (i 0; i FILTER_LEN; i) { sum filter_buf[i]; } return sum / FILTER_LEN; }如果你的干扰是那种罕见的毛刺尖峰滑动平均反而会把毛刺的效果“摊”到好几个采样点上这时候更适合用中值滤波连续采5个值排个序取中间那个。中值滤波对脉冲干扰的剔除效果非常明显代价是每次都要排序但5个元素的排序开销在M7上几乎可以忽略。我的经验是这两种滤波不要同时上很深的窗口否则会引入比较大的相位延迟。在闭环控制里延迟意味着稳定性变差。做电流环的时候我通常只做一次轻量滑动平均窗口开4就算多了因为FOC对电流采样的实时性要求很高。4. 实战二多通道DMA连续采样Cache一致性是最大的坑4.1 单通道读取在高采样率下会卡死CPU前面说的rt_adc_read是单次转换模型发一个软件触发、等转换结束、读结果、再触发下一次。如果你要同时采集4路信号每路转换时间2微秒多四路轮询下来一圈至少十几微秒而且CPU全程都在“傻等”状态。这在高频控制里是不可接受的。正确的思路是让ADC工作在连续扫描模式多通道自动依次转换转换结果由DMA自动搬运到内存缓冲区整个过程CPU完全不参与。CPU只需要在处理完一批数据后从内存缓冲区里把结果取走就行。4.2 DMA配置的核心参数源地址、目标地址、长度在GD32H759上配置DMA搬运ADC数据本质上只要抓住三个关键点源地址是ADC数据寄存器目标地址是你的内存缓冲区数据长度是通道数乘以你要采集的批次数量。我这里给一个配置思路的示意代码具体函数名以你手上的GD32H7固件库为准#define ADC_SCAN_CH_NUM 4 #define ADC_DMA_BUF_SIZE 64 rt_uint32_t adc_dma_buf[ADC_DMA_BUF_SIZE]; static void adc_dma_config(void) { dma_single_data_parameter_struct dma_init_struct; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH0); dma_single_data_parameter_struct_init(dma_init_struct); dma_init_struct.periph_addr (uint32_t)ADC_RDATA(ADC0); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_addr (uint32_t)adc_dma_buf[0]; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.number ADC_DMA_BUF_SIZE; dma_init_struct.direction DMA_PERIPH_TO_MEMORY; dma_init_struct.periph_width DMA_PERIPH_WIDTH_32BIT; dma_init_struct.memory_width DMA_MEMORY_WIDTH_32BIT; dma_single_data_mode_init(DMA0, DMA_CH0, dma_init_struct); dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0); }这里为什么要用32位宽度因为ADC数据寄存器在GD32H7上是32位对齐的虽然有效数据只有低12位过采样后是16位但用32位访问最自然也不用担心字节对齐问题。DMA循环模式一旦开启只要ADC有新的转换结果DMA就会自动搬运到缓冲区满了之后自动从头开始不需要CPU重新配置。4.3 M7内核的Cache一致性问题坑过无数人这是我在M7内核上做DMA踩过最深的一个坑必须单独拿出来说。Cortex-M7带D-Cache也就是数据缓存。CPU读内存时会先看缓存写内存时也可能先写到缓存里之后才刷回真实物理内存。这对于CPU访问普通变量来说是性能提升但对于DMA就麻烦了——DMA直接操作物理内存绕过CPU的缓存。如果DMA往一个已经被CPU缓存过的内存地址写了数据CPU再读这个地址时看到的可能还是缓存里的旧数据而不是DMA刚写进去的新值。表现就是ADC明明在转DMA中断也触发了但你在中断里去读缓冲区读到的一整批都是0或者旧值好像DMA根本没干活一样。解决办法有几种按推荐度排序第一把ADC的DMA缓冲区放在非Cacheable的内存区域。GD32H7的BSP一般会初始化MPU你可以把某一片SRAM配置成非缓存。查一下RT-Thread BSP里有没有类似USER_RAM或者NO_CACHE的段定义有的话直接把adc_dma_buf放进去。第二每次DMA传输完成后在CPU读数据之前执行Cache无效化操作SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, sizeof(adc_dma_buf));这个函数的两个参数都必须32字节对齐否则无效化不了完整的缓存行。我一开始没注意对齐调试了一下午后来把缓冲区定义为ALIGN(32)才解决。第三最暴力的方式是直接关闭D-Cache。我不推荐这么做因为会让整个系统的性能大打折扣属于捡了芝麻丢西瓜。对于DAC的DMA问题镜像对称CPU把数据写到Cache里还没刷回内存DMA就已经去内存拿了旧数据。解决办法要么是写完之后主动SCB_CleanDCache要么同样把缓冲区放到非Cacheable区域。记住一句话只要你的数据要在CPU和DMA之间共享就必须优先考虑Cache一致性问题。这是M7上开发工控代码绕不过去的门。5. 实战三DAC输出固定电压与可控波形5.1 DHR寄存器是怎么帮你“预置”电压的DAC和ADC的寄存器结构不太一样。GD32H759的DAC内部有两套关键寄存器一个是数据保持寄存器DHR一个是数据输出寄存器DOR。软件写入DHR然后DAC在合适的时机把DHR的值锁存到DORDOR驱动实际的电压输出。为什么要搞两层寄存器因为DHR的存在让“预置数据”成为可能。你可以在DAC还没有被触发输出的时候先把下一拍要输出的电压值写到DHR里等定时器触发到来DAC瞬间把这个值输出到引脚上而不是从软件算好再慢慢写这样就能保证波形的时间精度。RT-Thread的rt_dac_write在底层就是把值写进12位右对齐格式的DHR寄存器。如果你对时序要求不高用这个就够。但如果你想做可调频率的正弦波输出就得绕过标准框架自己配置DAC的触发源和DMA。5.2 查表法生成正弦波定时器触发加DMA搬运DAC输出正弦波的经典方案是查表法。提前生成一个正弦波采样表比如128个点每个点对应0到360度的一个采样值。DAC每隔固定时间间隔从表里取一个点输出循环起来就是正弦波了。在GD32H759上比较优雅的实现是定时器产生触发事件DAC被触发后从DHR寄存器锁存数据到DOR同时DMA自动把正弦表里的下一个值搬运到DHR。这三个硬件协同工作CPU完全不用管输出波形的频率由定时器的触发周期决定。生成正弦波表的代码很简单#define SIN_TABLE_SIZE 128 rt_uint16_t sin_table[SIN_TABLE_SIZE]; void sin_table_init(void) { rt_uint16_t i; for (i 0; i SIN_TABLE_SIZE; i) { float voltage 1.65f 1.5f * sinf(2.0f * 3.1415926f * i / SIN_TABLE_SIZE); sin_table[i] (rt_uint16_t)(voltage / 3.3f * 4095.0f); } }这个表产生的是中心电压1.65V、幅度1.5V、也就是峰值0.15V到3.15V的正弦波数据。表格里的值直接对应DAC 12位分辨率。之后配置DMA方向为内存到外设源地址是sin_table目标地址是DAC的DHR寄存器。DMA开启循环模式定时器一到新的正弦值就自动进DHRDAC输出立刻更新。此时你能在示波器上看到干净稳定的正弦波而且CPU占用率几乎是0。从我实测的情况来看这种方案输出的波形质量取决于两个因素一是查表密度表越大波形越平滑二是定时器触发抖动GD32H759的定时器在这方面表现很好用高级定时器做触发源能得到相当稳定的输出。5.3 模拟电压输出在工控里的实际用法DAC输出正弦波看起来花哨工控里更常见的其实是输出一个可编程的直流电压。比如伺服驱动器的速度给定、变频器的频率给定很多场合就是用0到10V或者4到20mA的模拟信号传递的。MCU的DAC直接输出通常是0到3.3V如果要变成0到10V就需要一级运放把电压放大。放大之后还有一个问题DAC输出阻抗不是理想零带载能力弱必须经过电压跟随器或者运放缓冲才能去驱动后级电路。我做过一个变频器频率给定板的项目DAC输出电压经过运放放大后变成0到10V输出但这个模拟信号线上很容易感应到变频器I/O端的噪声。最后的解决方式是在输出端加一个RC低通滤波器截止频率放到1kHz左右既不影响10V信号的调节响应又能把高频噪声压下去。这个RC取值的思路是输出阻抗和滤波电容形成一个低通转折频率越低滤波效果越好但调节响应越慢。现场调试时可以在截止频率和响应速度之间找一个平衡点不是越大越好也不是越小越好。6. 模拟量设计电源噪声、时钟抖动与PCB布局6.1 ADC/DAC的精度往往不是芯片决定的而是电路设计决定的很多人拿到ADC/DAC数据去分析精度问题最后发现芯片本身指标没问题问题全部出在硬件设计上。ADC和DAC都是模拟电路对电源噪声和PCB布局的敏感度远超数字电路。最常见的噪声源有系统电源的开关纹波、数字IO翻转带来的窜扰、时钟源的抖动和相噪以及参考电压上的噪声。这些噪声进入ADC会表现为采样值的连续跳动进入DAC会表现为输出波形上的毛刺。很多人碰到ADC数据漂移第一反应是加软件滤波滤波参数越加越大最后数据倒是稳定了但真实信号也被抹平了。这种思路是反的。正确的排查顺序应该是先确认硬件设计没有问题再谈软件滤波。6.2 规避时钟抖动与电源噪声的三个PCB布局要点这几年我在模拟量板卡设计上踩了不少坑整理成三个比较普适的PCB布局要点直接照做就能省掉大部分麻烦第一模拟地分割要合理但不要“切碎了铺地”。模拟部分和数字部分使用独立的地平面在电源入口处通过磁珠或0欧电阻单点连接。特别要注意的是高速数字信号线的回流地路径不能穿越模拟区域的下方否则数字回流电流会从模拟地平面流过噪声全部耦合进模拟信号。第二参考电压和电源引脚的滤波电容要就近放置。ADC的VREF引脚旁边放一个1uF和100nF的组合电容DAC输出引脚同样如此而且走线尽量短、尽量粗直接回到参考地。这个电容的放置位置比容值大小更重要放在1厘米和放在3厘米之外实际效果差很多。第三时钟源和高速数字信号远离模拟电路。晶振、以太网PHY的时钟线、USB数据线这些快速翻转的信号不要贴着ADC输入和DAC输出走。如果板子空间实在紧张至少保证模拟信号线被地过孔包围形成一个屏蔽槽。还有一个属于加分项的建议ADC模拟输入前端加一个简单的RC低通滤波电阻一百欧左右、电容几纳法到几十纳法。这个滤波除了抗混叠之外还能限制外部噪声的带宽对SAR型ADC的动态性能有明显帮助。我见过一些产品直接把传感器信号裸奔到MCU引脚结果EMC测试的时候问题一堆加了RC之后瞬间清净。6.3 输入端口的保护电路也别省工控现场经常有强电干扰ADC输入引脚一旦被高压打穿MCU直接报废。在ADC输入前端加钳位保护是必须的用两个肖特基二极管一个接到3.3V一个接地把输入电压钳在安全范围内再串一个1k到10k的限流电阻。这样即便外部瞬间过压电流也被电阻限制住不会烧坏MCU内部结构。如果是DAC输出端口考虑到可能被外部灌入电压也应该加串阻。这个串阻不算大一百欧以内既保护了引脚又不至于影响输出精度。7. 常见问题与排查速查表我把实际调试中遇到的高频问题整理成一张表每一条后面都附带排查思路方便你快速定位问题。现象可能原因排查与解决办法找不到ADC设备menuconfig未使能驱动或BSP未注册检查rt-thread配置确认find(adc0)名字一致采样值永远是0通道未使能、GPIO未配置为模拟模式确认rt_adc_enable检查GPIO复用模式采样值跳得厉害参考电压不稳、引脚悬空、采样时间不足检查VREF去耦电容外部信号加RC滤波增大采样时间多通道数据错位DMA缓冲区大小与通道数不匹配确保DMA长度是“通道数×批次数”DMA中断触发了但数据不变M7的Cache一致性问题缓冲区放入非Cache区域或执行SCB_InvalidateDCache_by_AddrDAC输出始终为0DAC使能未打开或DHR写入了0检查rt_dac_enable先写中间值测试DAC波形毛刺严重供电噪声、数字信号窜扰输出加RC滤波器检查参考电压PCB布局优化串口打印乱码波特率不匹配或USB转串口驱动问题确认串口配置重新安装CH340/CP2102驱动这些小问题单看都不难但组合在一起很容易让人怀疑人生。我的建议是在驱动调试阶段就做好“分步验证”先打开ADC读一路信号确认数值合理再开DMA确认搬运正确最后才上多通道和过采样。每一步都验证通过再往下走避免多个问题叠加在一起后根本无法定位。最后再分享一个我个人的习惯调试ADC/DAC的时候永远在硬件上留一个测试点把模拟输入信号引出来方便用示波器直接对比MCU读到的数值和真实波形。很多时候你以为是驱动写错了示波器一量才发现是前端电路的问题。有了这个对照排查效率至少翻一倍。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →