新闻详情

新闻详情

首页 / 资讯中心 / 详情

GD32H759实战:ADC/DAC采集输出与RT-Thread驱动详解

发布时间:2026/9/20 17:59:45来源:尧图网络
GD32H759实战:ADC/DAC采集输出与RT-Thread驱动详解
前面两篇把GD32H759的工程模板、GPIO、串口这些基础部分捋顺之后这一篇终于轮到模拟量了。在工控现场ADC和DAC几乎是绕不开的一对组合传感器给的是4~20mA电流或者0~10V电压得靠ADC变成数字量给CPU算算完之后要控制变频器、比例阀或者仪表又得靠DAC输出模拟量。所以这一篇我直接把GD32H759的ADC和DAC都讲透包括硬件设计要考虑的东西、RT-Thread设备驱动怎么对接、实际采样和输出会遇到哪些坑全部揉在一起照着做就能跑起来。无论你是刚从STM32转过来还是已经把GD32H759当裸机单片机用过一阵子这篇文章都能帮你少走弯路。GD32H759的ADC模块说实话比ST的H7系列更接近传统GD32风格初始化套路熟悉之后上手很快DAC部分则要格外注意触发方式和DHR寄存器这两件事很多人在这一步被卡住。1. 先把需求和硬件底子摸清楚1.1 工控场景里ADC/DAC到底在干什么别一上来就写代码。我见过太多人拿到板子就开始点寄存器最后做出来的东西在现场根本没法用。我们先理清楚工控环境里的模拟量输入输出和实验室里拿个电位器拧一拧完全是两码事。ADC端的典型需求是采集4~20mA变送器信号压力、温度、液位、采集0~10V或0~5V标准电压信号、采集NTC热电阻或热电偶的微弱电压。这些信号有几个共同特征一是现场干扰大电机启停、变频器PWM都可能把噪声耦合进来二是信号源阻抗可能很高尤其热电偶和某些电流环电路三是用户对精度有硬指标比如0.5%级仪表就要求12位ADC加上合理的信号调理误差不能随便漂。DAC端的典型需求则是输出0~10V控制变频器频率、输出4~20mA驱动阀门定位器、或者直接产生一个可编程的电压基准给测试台用。这些场景对DAC的要求没有ADC那么苛刻但因为DAC输出要驱动外部负载输出缓冲、驱动能力和输出电压范围必须单独考虑。GD32H759自带3个12位ADC和2个12位DAC从外设数量上说单芯片覆盖这些场景完全够用。有人会问既然要跑RT-Thread为什么还要用MCU自带的ADC而不外挂一个高精度ADC芯片答案很简单成本和响应速度。MCU内置ADC在大多数中小型工控设备里精度足够而且数据直接进内存、进总线延迟极小外挂ADC芯片通常是24位高精度场景才需要而且驱动复杂度会明显上升。1.2 GD32H759的ADC/DAC外设能力GD32H759这一代芯片主核是Cortex-M7跑600MHz外设资源相当豪横。具体到ADC它有三个独立的ADC模块ADC0、ADC1、ADC2每个都可以配置为12位分辨率支持多种触发源包括定时器触发和外部引脚触发每个模块有十几个外部输入通道。DAC部分是两路独立的12位DAC输出支持三角波和噪声波生成模式支持定时器触发和DMA搬运数据。此外还有模拟看门狗、过采样硬件求平均、规则组和注入组这些标准高级外设都具备的功能。对工控项目来说三个ADC最实用的价值是可以并行采集比如一路采集电机相电流一路采集母线电压另一路采集温度互不干扰。DAC两路可以分别输出不同的控制电压两个阀门或者两台变频器就能同时控。ADC的硬件过采样值得一提。12位ADC配合最高32倍过采样理论上能把有效位数往上提虽然达不到传闻中“等效16位”那么夸张但在信号比较平稳的工控场景里噪声抑制效果非常明显。我实际测试过环境干扰较强时打开4倍过采样之后采样数据的跳动幅度能从十几个LSB压到三四个LSB。这个功能后面我会详细讲怎么配。1.3 为什么要用RT-Thread设备驱动框架而不是裸机寄存器直操作很多从裸机转过来的工程师第一次看到RT-Thread的设备框架会有点抵触觉得多了一层封装跑起来不如直接写寄存器爽快。我的看法是裸机直接操作寄存器适合Demo和验证但一旦项目进入正轨任务一多设备框架的优势就出来了。RT-Thread把ADC设备抽象成rt_adc_read、rt_adc_enable、rt_adc_disable这几个接口把DAC设备抽象成rt_dac_write、rt_dac_enable这些接口。应用层的代码完全不需要关心底层寄存器细节换芯片平台时只要驱动层对接好了应用层一行都不用改。对于工控产品这种动辄维护三五年的项目这种解耦带来的维护红利非常大。另一个实际原因RT-Thread的rt_device_find机制可以让你在应用层用字符串名字找设备配合msh命令行调试非常方便。调试ADC的时候直接敲一行命令就能读回采样值不用每次重新编译烧录。这个体验一旦用上就回不去了。2. 硬件设计与引脚分配动手前的关键一步2.1 引脚和通道怎么选GD32H759的ADC引脚通常是PA0~PA7对应ADC0的通道0~7PB0、PB1对应通道8、9PC0~PC5对应通道10~15DAC0输出在PA4DAC1输出在PA5。具体到某个封装可能略有差异拿到板子第一件事就是打开原理图确认引脚别凭记忆接。选通道有几个原则。第一尽量把强干扰源和模拟输入引脚隔离开。比如PWM输出引脚和ADC输入引脚不要挨在一起否则采样数据里全是毛刺。第二如果你要用三个ADC并行采集三个通道要分布好避免一个ADC模块切换通道带来的串扰。第三DAC输出引脚和ADC输入引脚也要保持距离DAC输出的是模拟电压如果走线太长又和ADC输入走线平行很容易把噪声耦合进采样通道。GPIO模式必须配置成模拟输入这一点特别容易忘。用GD32标准库的话是gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_1)注意要关掉上下拉。模拟输入本来就应该是高阻态如果漏配成浮空输入虽然也能工作但耐压和噪声特性都不对。2.2 采集链路采样电阻、运放和RC滤波采集4~20mA电流信号最常规的做法是串一个250Ω的精密电阻把电流转成1~5V电压。问题是GD32H759的ADC供电电压是3.3VVDDA1~5V直接进ADC会超量程必须分压或者用运放做电平转换。简单一点的做法是分压。要在1~5V输入下把满量程压到3.3V以内分压比取0.66左右。取R112kΩ、R223.2kΩ5V输入时分压输出约3.295V留了一点余量。但分压之后的源阻抗大约是7.7kΩ直接进ADC会拖慢采样建立时间所以后面最好加一个运放跟随器缓冲也可以用仪表放大器直接做差分转单端。0~10V电压采集更简单分压比取1/3用R120kΩ、R210kΩ10V输入时分压输出约3.333V。同样建议加跟随器。RC低通滤波是ADC前端必不可少的现场信号的干扰频率通常远高于信号本身。以100Hz的信号为参考取R1kΩ、C100nF截止频率约1.59kHz既能有效衰减高频干扰又不会把有用信号压瘪。如果你采集的是更慢的温度信号C还可以加大到1uF截止频率降到159Hz抗干扰更强。ADC源阻抗对采样精度的影响很多人不重视。12位ADC要求采样电容在采样窗口内充到最终值的0.024%以内这需要大约8.3个时间常数的时间。假设外部源阻抗是10kΩADC内部采样电容约10pF时间常数就是100ns8.3个时间常数约830ns。如果ADC时钟36MHz、采样时间配15个周期只有约417ns那就明显不够测出来会偏小且不稳定。所以要么加运放把源阻抗压到几百欧以内要么把采样时间调到更长的档位。2.3 输出链路DAC缓冲、放大与保护DAC的输出路径很多人只关心DHR寄存器里写多少忽略了后面的模拟电路。GD32H759的DAC模块内部有输出缓冲开启之后可以直接驱动一定负载但带载能力有限。要驱动更重的负载比如控制一个电压输入的变频器外部最好再放一级运放跟随器。如果DAC要输出0~10V而MCU供电是3.3V就必须用运放把0~3.3V放大到0~10V。取放大倍数约3.03倍可以用同相放大电路Rf20kΩ、Rg10kΩ实际放大3倍最大输出9.9V够用。DAC输出端也需要保护。外部接线端子万一碰到强电一瞬间就能把MCU引脚打坏。常规做法是在DAC输出引脚上串一个100Ω电阻再接一个TVS管或者肖特基二极管钳位到3.3V电源轨。这样就算外部误接24V电流也会被TVS吸收而不是直接灌进MCU。对ADC输入端同样要加钳位保护现场长长的信号线就是一根天线静电和浪涌随时可能打进来。ADC输入并联的TVS电容不能太大否则会影响信号上升时间常规选几pF到几十pF的低结电容TVS加上原有的RC滤波电容保护效果足够。3. ADC驱动从寄存器到RT-Thread设备3.1 初始化配置时钟、分辨率、对齐、采样时间ADC初始化的关键流程我直接给出一段可参考的GD32标准库代码配置ADC0的通道1PA1为12位右对齐软件触发单次转换模式void adc0_init(void) { rcu_periph_clock_enable(RCU_ADC0); rcu_periph_clock_enable(RCU_GPIOA); /* PA1 模拟输入, 对应 ADC0_IN1 */ gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_1); adc_deinit(ADC0); /* 独立工作模式 */ adc_sync_mode_config(ADC0, ADC_SYNC_MODE_INDEPENDENT); /* 数据右对齐 */ adc_data_alignment_select(ADC0, ADC_DATAALIGN_RIGHT); /* 12位分辨率 */ adc_resolution_config(ADC0, ADC_RESOLUTION_12B); /* 规则组只配1个通道 */ adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); /* 配置通道1, 排在规则组第1个位置, 采样时间用15个周期 */ adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_1, ADC_SAMPLETIME_15); /* 无外部触发, 纯软件触发 */ adc_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ADC_EXTERNAL_TRIGGER_MODE_NONE); /* 使能连续转换 */ adc_continuous_mode_enable(ADC0); /* 使能ADC */ adc_enable(ADC0); /* 开启校准, 必须先使能后校准 */ adc_calibration_enable(ADC0); /* 触发第一轮转换 */ adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); }这段代码里最关键的两步是adc_enable和adc_calibration_enable的顺序。先使能、再校准这是GD32系列一个约定俗成的步骤顺序反了校准值可能无效。校准完成之后ADC内部的失调误差会被修正直接关系到底数准不准。采样时间的选择我给个通用建议源阻抗低、信号频率高就用短采样时间源阻抗高、信号频率低或者追求稳定性就用长采样时间。15个周期是一个比较折中的值信号源经过运放跟随之后完全够用。3.2 触发方式与多通道扫描DMA软件触发适合单通道或低频轮询但工控项目里多通道连续采集是常态。比如我要同时看4路传感器每路每秒才变化几十次如果每次都用软件触发串行去读CPU的时间就全耗在等待转换上了。这时候正确的做法是配置DMA让硬件自己把多通道的转换结果搬到内存数组里CPU在后台该干嘛干嘛。多通道DMA的配置套路如下先把规则组长度改成4依次把4个通道排进去然后使能DMA请求配置DMA为循环模式源地址指向ADC规则数据寄存器目标地址指向一个uint16_t adc_values[4]的数组最后软件触发一次之后ADC每转换完一个通道就会通过DMA把数据搬到内存。uint16_t adc_values[4] {0}; void adc0_multi_dma_init(void) { /* 参考单通道初始化, 将通道长度改为4 */ adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 4); adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_1, ADC_SAMPLETIME_15); adc_regular_channel_config(ADC0, 1, ADC_CHANNEL_2, ADC_SAMPLETIME_15); adc_regular_channel_config(ADC0, 2, ADC_CHANNEL_3, ADC_SAMPLETIME_15); adc_regular_channel_config(ADC0, 3, ADC_CHANNEL_4, ADC_SAMPLETIME_15); /* 使能DMA请求 */ adc_dma_mode_enable(ADC0); /* DMA初始化: 外设地址为 ADC0_RDATA, 内存地址为 adc_values, 长度4, 循环模式 */ dma_single_data_parameter_struct dma_param {0}; dma_param.periph_addr (uint32_t)(ADC_RDATA(ADC0)); dma_param.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_param.memory_addr (uint32_t)adc_values; dma_param.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_param.direction DMA_PERIPH_TO_MEMORY; dma_param.number 4; dma_param.periph_width DMA_PERIPH_WIDTH_16BIT; dma_param.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init(DMA0, DMA_CH0, dma_param); dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0); adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); }DMA搬运到内存后应用层直接读adc_values[0]到adc_values[3]就是4个通道的原始值不需要任何CPU介入。如果希望这4个通道固定以某个频率刷新配合定时器触发ADC即可这个后面会在DAC部分一起讲。3.3 硬件过采样怎么用GD32H759的ADC过采样模块本质上是对多次转换结果做累加和移位等效为硬件级的滑动平均。它和软件滤波的区别在于硬件过采样不占CPU时间而且内部处理逻辑比软件循环更快不会出现数据断流。过采样配置有两个关键参数过采样倍率和位移位数。过采样倍率是累加的次数一般支持2、4、8、16、32倍。位移位数是把累加结果右移的比特数右移位数决定了最终数据的有效位宽。比如12位ADC做16倍过采样累加结果最大有16位右移4位后仍然是12位数据但等效噪声降低如果只右移0位结果会变成16位分辨率看起来更高但实际只有低4位是噪声平均出来的伪精度功耗和转换时间却实实在在增加了。工控场景我推荐4倍过采样配合右移2位输出还是12位但数据稳定性提升非常明显。如果你对响应速度不敏感可以开到16倍过采样数据基本就是稳稳的。/* 使能16倍过采样, 右移4位 */ adc_oversample_mode_enable(ADC0); adc_oversample_shift_config(ADC0, ADC_OVERSAMPLE_SHIFT_4BIT); adc_oversample_ratio_config(ADC0, ADC_OVERSAMPLE_RATIO_16);配置过采样之后ADC的转换时间会成倍增加这一点务必记牢。16倍过采样意味着完成一次有效转换要等16次内部转换全部结束如果信号本身变化很快过采样反而会造成信号滞后。所以在做电机电流环这种高频场合过采样要慎用。3.4 对接RT-Thread ADC设备框架RT-Thread的ADC设备框架抽象非常干净驱动层只需要实现两个回调enabled和convert。我基于GD32标准库直接给出一个可用的适配代码#include rtdevice.h #include gd32h7xx.h static struct rt_adc_device adc0_device; static rt_err_t _adc0_enabled(struct rt_adc_device *dev, rt_uint32_t channel, rt_bool_t enabled) { if (enabled) { /* 使能对应通道的模拟输入, 实际GPIO配置在init里已经完成 */ } else { /* 通道禁用处理 */ } return RT_EOK; } static rt_err_t _adc0_convert(struct rt_adc_device *dev, rt_uint32_t channel, rt_uint32_t *value) { uint16_t raw 0; adc_regular_channel_config(ADC0, 0, (uint8_t)channel, ADC_SAMPLETIME_15); adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); while (SET ! adc_flag_get(ADC0, ADC_FLAG_EOC)) { /* 等待转换结束 */ } adc_flag_clear(ADC0, ADC_FLAG_EOC); raw (uint16_t)adc_regular_data_read(ADC0); *value raw; return RT_EOK; } static struct rt_adc_ops adc0_ops { .enabled _adc0_enabled, .convert _adc0_convert, }; int rt_hw_adc0_init(void) { adc0_init(); rt_hw_adc_register(adc0_device, adc0, adc0_ops, (void *)ADC0); return RT_EOK; } INIT_BOARD_EXPORT(rt_hw_adc0_init);这里有一个RT-Thread的坑必须提醒rt_adc_read返回的值不是原始转换结果的直接值而是框架统一做了16位对齐。12位ADC的原始值范围是0~4095对齐之后变成0~65520也就是原始值左移了4位。很多人在应用层写float voltage rt_adc_read(adc, 1) * 3.3f / 4095;结果算出来的电压明显偏大这就是忘了除以16。正确写法是float voltage rt_adc_read(adc, 1) / 16 * 3.3f / 4095;或者说先除以16还原原始值。3.5 应用层读取与数值换算设备注册好之后应用层就是标准的RT-Thread三板斧先找设备再使能通道最后读取。我习惯把“原始值转物理量”的换算单独封装一个函数这样上层只管电压、电流不用关心ADC细节。#include rtdevice.h #define ADC_DEV_NAME adc0 #define ADC_CHANNEL_PRES 1 /* PA1 */ #define ADC_REF_VOLTAGE 3.3f #define ADC_RESOLUTION 4095 static rt_adc_device_t adc_dev RT_NULL; float adc_read_voltage(void) { rt_uint32_t raw_aligned 0; rt_uint16_t raw 0; float voltage 0.0f; if (adc_dev RT_NULL) { adc_dev (rt_adc_device_t)rt_device_find(ADC_DEV_NAME); if (adc_dev RT_NULL) { return -1.0f; } rt_adc_enable(adc_dev, ADC_CHANNEL_PRES); } raw_aligned rt_adc_read(adc_dev, ADC_CHANNEL_PRES); raw (rt_uint16_t)(raw_aligned / 16); /* 还原12位原始值 */ voltage (float)raw * ADC_REF_VOLTAGE / ADC_RESOLUTION; return voltage; }如果是4~20mA经过250Ω电阻转成1~5V再分压最终计算电流时要把分压比和电阻值一起折算回去。我习惯在代码里把链路写清楚这样几个月后自己回来看也能一眼明白。实测下来用这个流程采一个稳定的直流电压12位分辨率下波动基本控制在一两个LSB以内配合过采样更稳。4. DAC驱动从DHR寄存器到模拟波形4.1 初始化配置时钟、触发源、输出模式DAC的初始化比ADC简单但触发方式的选择直接决定了后面能不能做定时器触发和DMA波形输出。先给一个最基础的单通道软件控制版本void dac0_init(void) { rcu_periph_clock_enable(RCU_DAC); rcu_periph_clock_enable(RCU_GPIOA); /* PA4 模拟输出, 对应 DAC0_OUT */ gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_4); dac_deinit(DAC0); /* 不使用波形发生器, 直接输出恒定电平 */ dac_wave_mode_config(DAC0, DAC_WAVE_DISABLE); /* 触发源选软件, 或者直接禁止触发 */ dac_trigger_source_config(DAC0, DAC_TRIGGER_SOFTWARE); dac_trigger_enable(DAC0); /* 使能输出缓冲, 增强带载能力 */ dac_output_buffer_enable(DAC0); /* 先输出0V, 12位右对齐 */ dac_data_set(DAC0, DAC_ALIGN_12B_R, 0); dac_enable(DAC0); }输出缓冲建议默认打开。GD32的DAC输出缓冲开启后输出阻抗明显降低驱动能力增强虽然会带来一点点增益误差和失调误差但绝大多数工控场景下利大于弊。只有你要做高精度低噪声输出时才会考虑关闭缓冲、外部再加精密运放。4.2 重点理解DHR数据保持寄存器DAC的DHRData Holding Register寄存器是这个外设的“灵魂”。12位模式下数据不是直接写进最终输出寄存器DOR的而是先写到DHR再由触发事件搬运到DOR最终体现在模拟输出引脚上。这个设计让MCU可以在DAC输出还没更新的时候提前把下一个值准备好从而实现“数据更新不占用CPU、由硬件定时触发”的效果。DHR寄存器有几种对齐形式8位右对齐、12位右对齐、12位左对齐。最常见的12位右对齐方式下写入0对应输出0V写入4095对应输出满量程接近VREF。如果误用了8位模式写入的数值就被截断成8位精度输出分辨率会明显下降。这一点在写代码时要注意dac_data_set的第二个参数要对齐模式第三个参数才是数值。软件触发模式下写DHR后数据会立即更新到DOR并体现在引脚上。如果你配置了外部触发或者定时器触发那么写DHR只是“准备好”要等触发信号到来才会真正更新输出。这个机制就是我们做定时器驱动波形输出的核心原理。4.3 对接RT-Thread DAC设备框架RT-Thread DAC框架的适配和ADC类似核心回调是enabled和write。我给出DAC0的适配代码static struct rt_dac_device dac0_device; static rt_err_t _dac0_enabled(struct rt_dac_device *dev, rt_uint32_t channel, rt_bool_t enabled) { if (enabled) { dac_enable(DAC0); } else { dac_disable(DAC0); } return RT_EOK; } static rt_err_t _dac0_write(struct rt_dac_device *dev, rt_uint32_t channel, rt_uint32_t value) { /* 限制在12位范围内 */ dac_data_set(DAC0, DAC_ALIGN_12B_R, value 0xFFF); return RT_EOK; } static struct rt_dac_ops dac0_ops { .enabled _dac0_enabled, .write _dac0_write, }; int rt_hw_dac0_init(void) { dac0_init(); rt_hw_dac_register(dac0_device, dac0, dac0_ops, (void *)DAC0); return RT_EOK; } INIT_BOARD_EXPORT(rt_hw_dac0_init);应用层使用DAC同样三步走void dac_write_voltage(float voltage) { rt_dac_device_t dac_dev (rt_dac_device_t)rt_device_find(dac0); if (dac_dev RT_NULL) { return; } rt_dac_enable(dac_dev, 0); /* 0~3.3V映射到0~4095 */ rt_uint32_t value (rt_uint32_t)(voltage * 4095.0f / 3.3f); if (value 4095) { value 4095; } rt_dac_write(dac_dev, 0, value); }注意RT-Thread的rt_dac_write不会自动做位宽对齐传进去多少就是多少所以上层必须自己保证数值范围。如果应用层习惯用16位满量程0~65535来思考在写回驱动之前要先右移4位变成12位值这个和ADC那头的方向正好相反。4.4 定时器触发DMA生成正弦波DAC配合定时器触发和DMA可以做到完全不占CPU的连续波形输出。工控里最常见的应用就是低频正弦波扫频测试或者用三角波做阀门抖动控制。我把核心思路说一下。先准备一个正弦波查找表比如256点每个点对应12位DAC值。然后配置DMA从正弦波表循环搬运数据到DAC的DHR寄存器DMA每搬运完一个数据DAC根据配置的触发源更新一次输出。如果以定时器作为DAC触发源定时器更新事件的频率就是DAC输出波形的更新频率。#define WAVE_POINTS 256 uint16_t sine_table[WAVE_POINTS]; void sine_table_init(void) { for (uint16_t i 0; i WAVE_POINTS; i) { double angle 2.0 * 3.1415926535 * i / WAVE_POINTS; double val (1.0 sin(angle)) / 2.0; /* 0~1 */ sine_table[i] (uint16_t)(val * 4095.0); } }DMA配置上源地址指向sine_table目标地址是DAC0的DHR寄存器地址传输宽度16位数量256循环模式。然后定时器触发源选择成DAC的触发输入定时器更新中断频率根据需要设置。整个过程中CPU只需要启动一次DMA后面波形就会一直循环输出。有一点要特别注意DAC的DMA请求方向和ADC不一样。ADC是外设主动请求DMA把结果搬走DAC是DMA把数据搬到DHR后还要等触发信号来了才会输出。所以使用定时器触发DAC时要把定时器的更新事件同时作为DAC触发源并且DMA搬运要和定时器节奏对齐否则可能出现波形错位。实测下来用这种方法输出1kHz正弦波CPU占用几乎可以忽略波形在示波器上看很干净。如果输出出现台阶感可以在DAC后面加一个低通滤波器把高频阶梯滤掉这也就是热词里常说的DAC插值数字滤波器思路模拟电路实现就是一个RC低通或者运放组成的二阶低通。5. 软件滤波与校准实战5.1 几种好用的C语言滤波函数ADC采样值抖得厉害的时候先别急着怀疑硬件很多时候软件加一道滤波就解决了。但滤波不能盲目加选错滤波器会导致信号滞后严重控制系统跟着一起抖。滑动平均滤波是最常用的对周期性干扰抑制效果不错缺点是数据会滞后约(N-1)/2个采样周期。采样点数N取4~8比较合适点数太多滞后就大了。中位值平均滤波则是先去掉最大值和最小值再求平均适合有随机尖峰脉冲干扰的场合工控现场电磁干扰打出来的毛刺用这个最有效。一阶低通滤波也叫惯性滤波实现最简单适合平滑变化缓慢的模拟量信号尤其适合温度、液位这种大惯性对象。我直接把三种滤波的代码一起给出/* 滑动平均滤波, N为采样点数 */ #define FILTER_N 8 uint16_t sliding_average_filter(uint16_t new_value) { static uint16_t buf[FILTER_N] {0}; static uint8_t index 0; static uint32_t sum 0; sum - buf[index]; buf[index] new_value; sum buf[index]; index (index 1) % FILTER_N; return (uint16_t)(sum / FILTER_N); } /* 中位值平均滤波 */ #define FILTER_M 5 uint16_t median_average_filter(uint16_t *buf, uint8_t len) { uint16_t temp[FILTER_M]; uint16_t sum 0; /* 拷贝并排序 */ for (uint8_t i 0; i len; i) { temp[i] buf[i]; } for (uint8_t i 0; i len - 1; i) { for (uint8_t j 0; j len - i - 1; j) { if (temp[j] temp[j 1]) { uint16_t t temp[j]; temp[j] temp[j 1]; temp[j 1] t; } } } for (uint8_t i 1; i len - 1; i) { sum temp[i]; } return (uint16_t)(sum / (len - 2)); } /* 一阶低通滤波 */ uint16_t lpf_filter(uint16_t new_value, uint16_t prev_value, float alpha) { return (uint16_t)((float)prev_value alpha * ((float)new_value - (float)prev_value)); }一阶低通的alpha取值很关键alpha越小滤波越强但滞后越大。一个可参考的经验是采样率为1kHz时alpha取0.2左右对100Hz以内的信号影响很小但能把500Hz以上的噪声压掉不少。5.2 ADC/DAC的软校准两点线性校正芯片的ADC和DAC都存在增益误差和失调误差手册里给的参数通常是±几LSB。如果项目要求0.5%甚至0.2%的精度不做校准是难以达标的。好在软件两点校准就能解决大部分问题。ADC校准的思路输入端加两个已知的精密电压比如V10.5V、V23.0V分别读出两次采样值raw1、raw2。然后建立线性映射y k×raw b其中k和b由这两个点求出。之后每次读到的raw都代入这个公式得到的y就是校准后的电压值。typedef struct { float k; float b; } calib_param_t; calib_param_t adc_calib_calc(uint16_t raw1, uint16_t raw2, float volt1, float volt2) { calib_param_t param; param.k (volt2 - volt1) / (float)(raw2 - raw1); param.b volt1 - param.k * (float)raw1; return param; } float adc_calib_convert(calib_param_t *param, uint16_t raw) { return param-k * (float)raw param-b; }DAC校准的方向相反是给DAC写两个已知数字量然后用万用表测实际输出电压依然两点拟合出数字量到实际电压的线性关系。应用层输出时把目标电压先逆映射成数字量再写入DAC。这个逆映射其实就是解反函数同样用两点法求。校准务必在系统上电稳定后做而且要等芯片温度基本稳定。运行一段时间后温漂是正常的如果项目要求全程高精度只能换更高规格的基准源和精密运放软件校准只能解决固定偏移和增益问题救不了温漂。6. 常见问题与排查实录6.1 采样值跳动、漂移、满偏ADC采样值乱跳排在第一位的原因永远是“电压基准或地没处理好”。ADC的VDDA引脚必须干净旁边要放去耦电容AGND和DGND在PCB上要单点连接。如果电源纹波大采样值就会跟着抖。第二个常见原因是采样时间配短了外部源阻抗又高采样电容没充到位测出来的值就忽大忽小。这时候把采样时间从15个周期改成55个周期甚至更长一般能明显改善。值恒为0或者恒为满量程那就是通道问题。恒为0先查是不是使能了过采样但没配位移或者通道号写错引脚根本没接进来恒为满量程通常是引脚悬空或者内部上拉没关掉模拟输入引脚必须配成无上下拉的模拟模式。漂移问题多半来自温度。芯片刚上电的时候会有一点自热ADC读到的值随着时间缓慢变化这是正常现象软件开机后等几秒再做一次校准能缓解。如果漂移量大得离谱查一下是不是有强干扰源把基准电压污染了。6.2 DAC输出非线性、电压不对DAC输出电压不是目标值先确认你往DHR里写的数据格式对不对。12位右对齐模式下写4095才对应满量程如果打成255之类的小值输出自然不对。另一个常见问题是输出缓冲没开导致接上负载后电压被拉低示波器一看波形是“塌”的这时候把dac_output_buffer_enable打开就好了。如果DAC输出带载后波形振荡多半是输出缓冲和外部运放配合不稳定。排查办法是外部运放反馈电阻不要太小至少选几kΩ级别同时在输出端加一个小电容吸收振铃。DAC输出波形有台阶感是正常现象DAC本身就是零阶保持输出输出恒定电压直到下一个更新点。台阶感太明显就加低通滤波或者提高DAC的更新率让每一步台阶更小更密。6.3 RT-Thread设备访问的坑RT-Thread最常见的问题是设备找不到。rt_device_find(adc0)返回空指针十有八九是驱动初始化没有执行。检查一下INIT_BOARD_EXPORT宏有没有把它挂到自动初始化流程里如果用线程栈调试确认初始化确实在读取之前跑完了。还有个坑是设备框架的rt_adc_read返回16位对齐值前面已经强调过这里再提醒一次。很多人辛辛苦苦调了半天最后发现算出来的电压偏大16倍就是没除这个16。DAC那边反过来写进去的值不会帮你移位两边机制完全不同移植代码时要格外留意。6.4 调试环境驱动问题用RT-Thread Studio调试时最容易出问题的是调试器驱动和串口驱动没装好。CH340、CP2102这类USB转串口芯片插上电脑后设备管理器里能看到COM口才能用ST-Link、J-Link这类调试器更要注意驱动版本不然连接目标板时报错会让人一头雾水。这些驱动安装包都很容易找到装完重启一次电脑基本都能解决。6.5 排查顺序建议我把常见问题整理成一个速查表排查时按这个顺序来可以少走很多弯路现象优先排查项次要排查项ADC值乱跳VDDA去耦、AGND连接采样时间、外部源阻抗ADC恒为0通道号、引脚配置过采样配置、触发方式ADC恒为满量程引脚上下拉、引脚悬空DMA传输宽度DAC电压偏低输出缓冲、负载电流DHR写入格式DAC波形振荡输出缓冲、运放反馈外部负载电容设备找不到初始化宏、自动初始化设备名字拼写数据偏大16倍rt_adc_read对齐处理应用层换算公式单独讲一个我踩过的坑有一次做DAC三角波输出发现波形频率完全不对以为是定时器配置错了折腾了半天。最后查出来是DMA配置里传输宽度写成了8位导致每次只搬运一个字节到DHR寄存器高8位数据全丢了。这种情况不会报错波形看起来“有点像”但完全不对最坑人。遇到波形异常先把DMA的宽度、数量、地址这三个参数逐一打印出来检查能省很多时间。我个人在实际操作中的体会是GD32H759的ADC和DAC外设本身不难难的是把“硬件链路、外设配置、RT-Thread框架、应用层换算”这四个环节串成一个闭环。每一个环节都可能埋坑但只要你按照“先硬件、再外设、再框架、再应用”的顺序去排查问题基本都能定位到具体某一行。这一篇的内容足够支撑你完成一个最基本的模拟量采集和输出系统了后续如果再深入可以把DMA环形缓冲、多ADC同步触发、以及DAC配合PID控制器的实时输出这些工程优化加进来。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SpringBoot+Vue智慧医疗挂号系统:从数据建模到号源防超卖的完整实现 2026/9/20 20:09:04

SpringBoot+Vue智慧医疗挂号系统:从数据建模到号源防超卖的完整实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
GitHub周刊第35周:图像生成工程化、架构可核验与AI编程CLI深度解析 2026/9/20 20:09:04

GitHub周刊第35周:图像生成工程化、架构可核验与AI编程CLI深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Windows 上 Ollama GPU 推理实战:从驱动配置到显存调优 2026/9/20 20:09:04

Windows 上 Ollama GPU 推理实战:从驱动配置到显存调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Handsontable 单元格渲染器(Cell Renderer)实战指南:从内置别名到自定义函数、注册与框架组件渲染器 2026/9/20 20:09:04

Handsontable 单元格渲染器(Cell Renderer)实战指南:从内置别名到自定义函数、注册与框架组件渲染器

Handsontable 单元格渲染器(Cell Renderer)实战指南:从内置别名到自定义函数、注册与框架组件渲染器 【免费下载链接】handsontable JavaScript Data Grid / Data Table with a Spreadsheet Look & Feel. Works with React, Angular, and…

阅读更多 →
Codex AI编程代理快速入门:从安装到第一条指令的完整指南 2026/9/20 20:09:04

Codex AI编程代理快速入门:从安装到第一条指令的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
轻量级电脑监控组合:TrafficMonitor+HWiNFO,老设备也能实时掌握五大硬件指标 2026/9/20 20:06:04

轻量级电脑监控组合:TrafficMonitor+HWiNFO,老设备也能实时掌握五大硬件指标

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞