
1. 从零搭建 GD32H759 的 ADC/DAC 驱动框架拿到 GD32H759 这块片子做工业控制项目绕不开的就是模拟量采集和模拟量输出这两件事。传感器送进来的 4-20mA 电流信号、0-10V 电压信号最终都要变成 MCU 能处理的数字量这就是 ADC 的活儿反过来控制执行器、调节阀、变频器给定又需要 MCU 输出精确的模拟电压或电流这就是 DAC 的活儿。我在这个项目里用的是 RT-Thread 操作系统所以 ADC 和 DAC 的驱动不能像裸机那样写个死循环就完事得按照 RT-Thread 的设备驱动框架来组织让上层应用通过标准的rt_device接口来操作。这一篇主要聊三件事第一GD32H759 的 ADC 和 DAC 外设在工控场景下怎么配置才靠谱第二RT-Thread 的设备驱动框架下ADC/DAC 驱动怎么写、怎么注册、怎么用第三实际调试过程中踩过的坑和解决办法。如果你手头正好有 GD32H7 系列的板子或者在做类似的工控数据采集项目这篇内容可以直接拿去参考。GD32H759 是兆易创新基于 Cortex-M7 内核的高性能 MCU主频能跑到 600MHz内置的 ADC 是 12 位逐次逼近型支持多达 20 多个外部通道DAC 是 12 位电压输出型有两路独立通道。这些参数放在工控场景里够用但前提是你得把采样时序、参考电压、滤波电路这些外围因素都考虑进去否则 12 位 ADC 实际有效位数可能连 10 位都不到。注意GD32H759 的 ADC 和 STM32H7 系列的 ADC 在寄存器层面有相似之处但并不完全兼容。直接拿 STM32 的代码来改时钟配置和校准流程上很容易出问题。1.1 为什么选 RT-Thread 而不是裸机工控项目里ADC 采集通常不是孤立运行的。你可能同时要跑 Modbus 通信、PID 控制算法、LCD 显示刷新、数据存储等多个任务。裸机写一个大循环ADC 采样的实时性很难保证稍微有个任务耗时长了采样周期就抖了。RT-Thread 的好处在于你可以把 ADC 采样放在一个独立线程里设定合适的优先级用信号量或者消息队列跟其他线程同步数据采样周期稳定性会好很多。另外RT-Thread 的设备驱动框架把 ADC 抽象成了rt_adc_device上层应用不用关心底层是 GD32 还是 STM32统一用rt_adc_enable、rt_adc_read这些接口就行。DAC 虽然没有像 ADC 那样在 RT-Thread 里有一个特别标准的设备类但我们可以参照字符设备或者杂项设备的思路来封装保持接口风格一致。我试过在裸机下用定时器触发 ADC DMA 搬运的方式确实也能做到很稳定的采样但代码耦合度高换个芯片平台基本要重写。用 RT-Thread 的话底层驱动改一改上层应用几乎不用动这在产品迭代时省事很多。1.2 工控场景对 ADC/DAC 的特殊要求工业现场跟消费电子不一样电磁环境复杂对可靠性和精度的要求高。具体到 ADC/DAC 这块有几个硬指标采样精度12 位是最低要求实际有效位数ENOB最好能到 10 位以上。这意味着参考电压要稳PCB 布局要讲究软件上可能还需要做数字滤波。采样速率工控里大多数模拟量变化不快温度、压力、流量这些物理量采样率几百 Hz 到几 kHz 足够了。但如果是振动监测或者电流环控制可能要求几十 kHz 甚至更高。隔离与保护现场信号进来之前硬件上要有隔离、限幅、滤波。ADC 引脚直接暴露在工业现场不加保护很容易被打坏。输出精度与驱动能力DAC 输出通常要经过运放缓冲直接驱动负载能力有限。如果要求 4-20mA 电流输出还需要额外的 V/I 转换电路。这些硬件层面的东西软件驱动里虽然不直接体现但你在配置采样时间、触发方式、滤波参数时必须心里有数。2. GD32H759 ADC 外设配置与 RT-Thread 驱动实现先看 ADC。GD32H759 的 ADC 模块功能挺全的支持规则通道和注入通道支持外部触发支持 DMA 搬运还有硬件过采样功能。工控场景下我最常用的配置是定时器触发 DMA 搬运 规则通道扫描这样可以在不占用 CPU 的情况下连续采集多个通道。2.1 ADC 时钟与参考电压配置GD32H759 的 ADC 时钟来源是 AHB 总线时钟经过分频得到的。这里有个坑ADC 时钟不能超过芯片手册规定的最大值具体数值查一下数据手册通常在 60MHz 左右。如果你把 AHB 跑到 600MHz分频系数设小了ADC 时钟超频采样结果会飘得厉害。参考电压方面GD32H759 有独立的 VREF 引脚工控板上一般会用一个高精度基准源比如 REF5025 或者 ADR4525输出 2.5V 或 4.096V。参考电压的选择直接影响 ADC 的分辨率分辨率 VREF / 4096如果用 3.3V 参考分辨率约 0.8mV用 4.096V 参考分辨率是 1mV。看起来 3.3V 分辨率更高但实际工控信号调理电路输出范围通常是 0-3V 或 0-5V你得根据信号范围来选参考电压让信号尽量占满 ADC 量程这样才能充分利用 12 位精度。提示VREF 引脚旁边一定要放 1uF 和 100nF 的退耦电容而且尽量靠近引脚。我见过因为退耦电容离得太远采样值跳变十几个 LSB 的情况。2.2 采样时间与通道配置GD32H759 的 ADC 采样时间是可编程的每个通道可以独立设置。采样时间太短采样保持电容充不满转换结果偏小采样时间太长整体采样率上不去。工控信号源内阻一般比较大经过调理电路后输出阻抗可能在几十 kΩ 级别这时候采样时间要设长一点。具体怎么算ADC 采样保持电容大概是几 pF 级别信号源内阻 R 和采样开关电阻构成 RC 充电电路。要保证充电误差小于 0.5 LSB时间常数 τ R × C采样时间至少需要 8-10 个 τ。假设 R 10kΩC 8pFτ 80ns采样时间至少 800ns。GD32H759 的 ADC 时钟周期如果是 20ns那采样时间至少设 40 个周期。实际配置时我一般先用一个偏保守的值比如 56 周期或者 144 周期然后用示波器看采样保持电容上的波形确认充电到位了再往下调。2.3 RT-Thread ADC 设备驱动框架RT-Thread 的 ADC 设备驱动框架定义在rtdevice.h里核心结构体是rt_adc_device操作接口是rt_adc_ops。我们要做的就是实现这些 ops然后把设备注册到系统里。struct rt_adc_ops { rt_err_t (*enabled)(struct rt_adc_device *device, rt_int8_t channel, rt_bool_t enabled); rt_err_t (*convert)(struct rt_adc_device *device, rt_int8_t channel, rt_uint32_t *value); rt_uint8_t (*get_resolution)(struct rt_adc_device *device); rt_int16_t (*get_vref)(struct rt_adc_device *device); };enabled用来使能或关闭某个通道convert用来读取某个通道的转换值get_resolution返回分辨率位数get_vref返回参考电压单位 mV。在 GD32H759 上实现这些接口底层就是操作 ADC 的寄存器和 GD32 的标准外设库函数。比如convert里面先选择规则通道启动转换等待 EOC 标志然后读数据寄存器。static rt_err_t gd32_adc_convert(struct rt_adc_device *device, rt_int8_t channel, rt_uint32_t *value) { uint32_t adc_periph ...; // 从 device 私有数据里取 adc_regular_channel_config(adc_periph, 0, channel, ADC_SAMPLETIME_56); adc_software_trigger_enable(adc_periph, ADC_REGULAR_CHANNEL); while(!adc_flag_get(adc_periph, ADC_FLAG_EOC)); adc_flag_clear(adc_periph, ADC_FLAG_EOC); *value adc_regular_data_read(adc_periph); return RT_EOK; }这段代码是轮询方式简单但效率低。如果要多通道连续采集建议用 DMA在驱动初始化的时候就把 DMA 配好convert接口直接返回 DMA 缓冲区里的最新值。2.4 多通道扫描与 DMA 搬运工控项目里经常要同时采集好几路模拟量比如三相电流、母线电压、温度等等。GD32H759 的 ADC 支持规则通道扫描模式配合 DMA 可以把多个通道的转换结果自动搬到内存数组里。配置步骤大致如下配置 ADC 规则通道序列把要采的通道按顺序排好。配置 DMA 通道源地址是 ADC 规则数据寄存器目标地址是内存数组传输长度等于通道数。使能 ADC 的 DMA 请求。启动 ADC 连续转换模式。在 RT-Thread 里开一个线程定期读取 DMA 缓冲区或者用 DMA 传输完成中断来通知。这里有个细节GD32H759 的 ADC 规则数据寄存器在扫描模式下每次转换完一个通道数据就会更新。DMA 请求是在每次 EOC 时产生的所以 DMA 传输长度要跟通道数匹配否则数据会错位。注意DMA 缓冲区建议用rt_malloc分配并且确保地址对齐。有些情况下DMA 访问非对齐地址会出问题虽然 Cortex-M7 支持非对齐访问但外设 DMA 不一定支持。2.5 硬件过采样与数字滤波GD32H759 的 ADC 有一个很实用的功能硬件过采样。它可以在硬件层面把多次采样结果累加求平均直接输出滤波后的值。这对于工控场景下抑制随机噪声非常有效。过采样配置涉及几个参数过采样倍数2x 到 256x、右移位数、过采样模式普通模式或累加模式。比如设置 16 倍过采样右移 4 位等效于把 16 次采样求平均信噪比理论上能提高 4 倍也就是 2 个有效位。但硬件过采样会降低采样率。16 倍过采样意味着完成一次转换实际上花了 16 个转换周期。如果你的采样率要求不高比如温度采集每秒几十次那用硬件过采样很划算。如果要求高速采样那就得权衡了。软件上也可以做滤波比如中值滤波、滑动平均滤波、一阶低通滤波。我一般会在驱动层之上再做一个简单的滑动平均窗口大小根据信号特性来定。工控信号里温度、压力这些慢变量窗口可以大一点比如 16 或 32电流、电压这些快变量窗口小一点4 或 8 就够了。3. GD32H759 DAC 驱动实现与输出控制DAC 这块RT-Thread 没有像 ADC 那样提供标准的设备类所以我们需要自己封装一套接口。我的做法是参照 ADC 的设备框架定义一个rt_dac_device结构体和对应的 ops然后注册成杂项设备或者字符设备。3.1 DAC 输出模式与触发源选择GD32H759 的 DAC 有两路输出每路可以独立配置。输出模式上可以选择正常模式或者采样保持模式。正常模式下写入数据寄存器后DAC 输出立即更新采样保持模式下需要外部触发信号来更新输出。工控场景下如果要做波形输出或者需要精确控制更新时刻就用定时器触发 DMA 搬运的方式。比如你要输出一个 1kHz 的正弦波就可以把正弦表放在内存里用定时器以 100kHz 的速率触发 DACDMA 依次把表中的值搬到 DAC 数据寄存器输出就是 1kHz 的正弦波。触发源的选择上GD32H759 的 DAC 支持多种触发源包括定时器 TRGO、外部中断线等。我一般用定时器触发因为定时器的周期可以精确控制而且跟 ADC 的触发可以同步方便做闭环控制。3.2 DAC 数据对齐与 DHR 寄存器GD32H759 的 DAC 数据寄存器是 12 位的但寄存器本身是 32 位的。数据对齐方式有右对齐和左对齐两种。右对齐时低 12 位有效左对齐时高 12 位有效。这个在配置的时候要注意写错了输出值会差很多倍。DHR 寄存器DAC Holding Register是实际写入数据的寄存器DAC 输出寄存器 DOR 是从 DHR 同步过来的。在正常模式下写 DHR 后经过几个时钟周期DOR 更新输出变化。如果使能了输出缓冲输出阻抗会降低驱动能力增强但输出范围会受限不能到轨到轨。提示DAC 输出缓冲使能后最小输出电压大概在 0.2V 左右最大输出电压比 VDDA 低 0.2V 左右。如果你需要输出接近 0V 或 VDDA 的电压要么禁用缓冲要么在外部加运放做电平偏移。3.3 RT-Thread 下 DAC 设备封装我封装的 DAC 设备接口大概长这样struct rt_dac_ops { rt_err_t (*enable)(struct rt_dac_device *device, rt_int8_t channel); rt_err_t (*disable)(struct rt_dac_device *device, rt_int8_t channel); rt_err_t (*set_value)(struct rt_dac_device *device, rt_int8_t channel, rt_uint32_t value); rt_uint32_t (*get_value)(struct rt_dac_device *device, rt_int8_t channel); };set_value里面就是写 DHR 寄存器get_value读 DOR 寄存器。初始化的时候配置好 GPIO、DAC 时钟、触发源等。注册设备的时候可以用rt_device_register注册成字符设备设备类型用RT_Device_Class_Miscellaneous。上层应用通过rt_device_find找到设备然后rt_device_open、rt_device_write来操作。不过rt_device_write的参数是void*和rt_size_t用来传 DAC 值不太直观所以我更倾向于直接暴露set_value函数或者用rt_device_control来传自定义命令。3.4 定时器触发与 DMA 波形输出如果要输出动态波形比如正弦波、三角波或者做任意波形发生器那就需要定时器 DMA 的配合。配置流程配置一个定时器设置好频率TRGO 输出连接到 DAC 触发输入。配置 DMA 通道源地址是波形数据数组目标地址是 DAC 的 DHR 寄存器。使能 DAC 的 DMA 请求和触发使能。启动定时器和 DMA。这里有个容易忽略的点DMA 传输完成之后如果还需要循环输出要把 DMA 配置成循环模式或者用传输完成中断重新装载。循环模式下DMA 会自动从头开始搬数据不需要 CPU 干预。波形数据的生成可以在初始化的时候用数学函数算好放在数组里。比如正弦波#define WAVE_POINTS 256 static uint16_t sine_wave[WAVE_POINTS]; for (int i 0; i WAVE_POINTS; i) { sine_wave[i] (uint16_t)(2048 2047 * sin(2 * 3.1415926 * i / WAVE_POINTS)); }这样生成的正弦波幅度范围是 1 到 4095中心点在 2048对应输出电压大概是 VREF/2。4. 调试实战常见问题与排查技巧驱动写完了烧进去跑不起来或者跑起来数据不对这是常态。下面整理几个我在 GD32H759 上调试 ADC/DAC 时经常遇到的问题和排查思路。4.1 ADC 采样值跳动大怎么办采样值跳动是最常见的问题。可能的原因和排查顺序现象可能原因排查方法跳动几个 LSB参考电压噪声用示波器看 VREF 引脚纹波跳动几十个 LSB信号源内阻太大采样时间不够加长采样时间或加电压跟随器跳动上百个 LSB地线干扰模拟地和数字地混在一起检查 PCB 布局单点接地偶尔跳变到满量程引脚受到干扰或配置错误检查通道配置加 RC 滤波我遇到过一次采样值一直在跳换了参考电压芯片也没用。后来发现是模拟地和数字地在 PCB 上通过一个 0 欧姆电阻连接但那个电阻离 ADC 太远地电流在走线上产生了压降。把电阻挪到 ADC 引脚附近问题就解决了。软件上如果硬件已经定型没法改那就只能靠数字滤波。一阶低通滤波是最简单的filtered filtered * 0.9 new_sample * 0.1;系数根据信号带宽来调截止频率越低滤波效果越好但响应也越慢。4.2 DAC 输出不准或带载能力差DAC 输出不准先检查参考电压和配置。如果配置没问题那大概率是输出缓冲的问题。禁用输出缓冲后DAC 输出阻抗比较高带载能力很差接个几 kΩ 的负载电压就掉下来了。这时候需要在外部加一个运放做电压跟随。另外DAC 的输出范围跟 VDDA 有关。如果 VDDA 是 3.3VDAC 最大输出大概就是 3.3V 减去缓冲的压降。如果你需要 0-10V 输出那必须外部加运放做放大。注意DAC 输出引脚在禁用状态下是高阻态如果外部电路有上拉或下拉可能会影响其他电路。设计时要注意。4.3 RT-Thread 设备注册失败排查RT-Thread 下设备注册失败通常有几个原因设备名重复、内存不足、设备结构体没有正确初始化。用rt_device_find找不到设备先检查rt_device_register的返回值如果是-RT_ERROR那可能是名字冲突了。还有一个容易忽略的点RT-Thread 的自动初始化机制。如果你用INIT_DEVICE_EXPORT宏来注册设备要确保链接脚本里包含了对应的段。有些自定义的链接脚本可能会把.rti_fn段丢掉导致初始化函数根本没被调用。我一般会在rt_device_register之后加一句rt_kprintf打印设备名和返回值确认注册成功了。然后在应用层用rt_device_find再确认一次。4.4 采样周期不稳定的问题用 RT-Thread 线程做 ADC 采样时如果采样周期要求严格不能用rt_thread_mdelay来定时因为线程调度和系统 tick 的精度有限。更好的做法是用硬件定时器触发 ADC或者用 RT-Thread 的软件定时器配合高精度时钟。如果采样周期要求不高比如几十毫秒那rt_thread_mdelay也够用。但要注意如果系统 tick 是 1msrt_thread_mdelay(10)实际延时可能是 10ms 到 11ms 之间有 1 个 tick 的抖动。对于工控里常见的 PID 控制采样周期抖动一点问题不大PID 算法本身对周期变化有一定的鲁棒性。但如果是做谐波分析或者需要精确同步的场合那就必须用硬件触发。4.5 中断优先级与实时性ADC 转换完成中断、DMA 传输完成中断的优先级要设置合理。如果这些中断优先级太低被其他中断打断可能会导致数据丢失或者采样周期抖动。RT-Thread 下中断优先级通过rt_hw_interrupt_set_priority或者直接操作 NVIC 来设置。我一般会把 ADC/DMA 中断优先级设得比较高但不要高过系统 tick 中断和关键的系统中断。具体优先级数值根据项目里其他中断的分布来定原则是保证 ADC 数据搬运不被耽误同时不影响系统的正常运行。5. 工控场景下的扩展思考与优化方向ADC/DAC 驱动跑通只是第一步真正放到工控现场还有很多可以优化的地方。5.1 校准与温度漂移补偿12 位 ADC 和 DAC 都有增益误差和偏移误差出厂时虽然有校准但精度要求高的场合还是需要自己做两点校准。具体做法是给 ADC 输入两个已知电压比如 10% 和 90% 量程记录 ADC 读数算出增益和偏移然后在软件里做补偿。温度漂移方面如果工控设备工作在宽温范围比如 -40 到 85 度那参考电压和 ADC 本身的温漂都会影响精度。高精度场合可以考虑用带温度补偿的基准源或者在软件里根据板载温度传感器做补偿。5.2 隔离与保护电路的配合软件驱动再稳硬件保护不到位也白搭。ADC 输入引脚前面我一般会加 TVS 管做浪涌保护加 RC 低通滤波做抗混叠加电压跟随器做阻抗匹配。如果是电流输入还要加采样电阻和信号调理电路。DAC 输出引脚如果接到外部设备最好加一个运放缓冲再加限流电阻和 TVS 管。工业现场执行器那边如果有感性负载断电瞬间的反向电动势可能会通过 DAC 输出引脚倒灌进来打坏芯片。5.3 与 Modbus 等通信协议的配合工控项目里ADC 采集的数据通常要通过 Modbus RTU 或者 Modbus TCP 上传给上位机或者 PLC。RT-Thread 下有现成的 Modbus 协议栈比如 FreeModbus 或者 agile_modbus。ADC 驱动采集到的数据经过滤波和校准后放到 Modbus 的寄存器映射表里上位机通过读保持寄存器就能拿到。DAC 输出值也可以映射到 Modbus 的写寄存器上位机写值DAC 驱动更新输出。这样整个模拟量采集和输出就通过标准协议跟外部系统打通了。5.4 低功耗模式下的 ADC/DAC 处理如果工控设备是电池供电或者有低功耗要求ADC 和 DAC 在空闲时应该关闭。GD32H759 的 ADC 和 DAC 都有独立的使能位不用的时候关掉可以省不少电。RT-Thread 下有 PM电源管理框架可以把 ADC/DAC 的电源管理挂进去系统进入低功耗模式时自动关闭外设。不过要注意ADC 和 DAC 重新使能后需要一定的稳定时间特别是参考电压和输出缓冲。如果低功耗模式切换频繁这个稳定时间可能会影响响应速度。5.5 代码结构与可维护性最后聊一下代码组织。ADC 和 DAC 的驱动代码我一般会分成三层底层寄存器操作、RT-Thread 设备接口、应用层 API。底层寄存器操作直接调用 GD32 的标准外设库RT-Thread 设备接口实现 ops 和注册应用层提供简洁的函数给业务代码调用。这样分层的好处是换芯片平台时只需要改底层RT-Thread 设备接口和应用层基本不动。另外调试信息用rt_kprintf输出方便在 RT-Thread Studio 的终端里看日志。生产版本可以把调试打印关掉减少代码体积和运行开销。我在实际项目里还加了一个简单的自检功能上电时读一次 ADC 的参考电压通道如果有内部参考的话跟预期值对比偏差太大就报错。DAC 也可以回读输出寄存器确认写入的值跟读回的值一致。这些小功能在批量生产时能帮你快速筛出硬件有问题的板子。这个 ADC/DAC 驱动框架在 GD32H759 上跑下来配合 RT-Thread 的线程和 IPC 机制做多通道数据采集和模拟量输出控制是足够稳的。后面如果要做更复杂的信号处理比如 FFT 分析或者数字滤波器可以在应用层再加算法模块驱动层不用动。