ARTICLE DETAIL

资讯详情

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

GD32H759 + RT-Thread:ADC/DAC驱动实战与踩坑记录

GD32H759 + RT-Thread:ADC/DAC驱动实战与踩坑记录 GD32H759 RT-Thread 的工控实战系列写到第3篇这回来聊聊ADC/DAC驱动。做工业控制的人应该都有体会模拟量采集和输出看起来简单真调起来比跑一个以太网协议栈还磨人。数字信号非0即1错了也好定位模拟信号偏个0.2V设备就给你停机。这篇文章不会重复数据手册里的寄存器表而是把我在实际项目中调GD32H759的ADC/DAC驱动的完整思路、代码骨架、踩坑过程都摊开讲包括多通道DMA怎么配、采样值漂了怎么查、DAC带不动负载怎么办以及最后怎么把这些驱动接到RT-Thread设备框架里。适合正在折腾GD32H7系列或者玩STM32H7想换GD32的人尤其是工控现场被模拟量折腾过的朋友。1. 先摸清GD32H759的模拟外设家底再谈驱动怎么写1.1 我为什么在GD32H759上做模拟量而不是外挂独立ADC/DAC项目刚开始评审方案时硬件同事提过一版方案MCU用便宜的GD32F303模拟量采集外挂ADS1256输出外挂DAC8562。理由是内置ADC/DAC精度不行还是独立芯片靠谱。我听完没有直接反对但让硬件同事把整套方案的成本、布线面积、软件复杂度都列出来。认真算了一笔账外挂方案多两片芯片、多一组隔离电源、多三路SPI程序里要单独调两套驱动还要处理SPI通信时序抖动。而我们的工控项目精度要求是12位够用4-20mA电流环采样用250Ω电阻转成电压后12位分辨率的LSB不到1mV配合一点软件滤波和校准完全能压进0.5%的误差要求。后来我们定了就在GD32H759上做。不是外挂方案不好而是这个项目用内置外设就能满足没必要让系统复杂度翻倍。至于网上很多人说的内置ADC漂移、内置DAC毛刺我实测发现大部分问题出在参考电压、电源噪声和PCB布局上芯片本身并没有那么不堪。这些问题后面会展开讲。1.2 这块芯片的ADC/DAC资源与工控场景匹配情况我不打算在这里复读数据手册里的分辨率、通道数表格你拿到板子第一件事应该是打开参考手册重点看ADC和DAC特性章节把和自己应用相关的参数标出来。GD32H759的ADC外设和同系列Cortex-M7芯片的整体架构类似有几个关键特性对驱动开发非常重要支持规则组和注入组两种转换组规则组适合周期扫描多通道注入组适合外部事件触发的高优先级单次采集。可以配置多个通道按顺序扫描也支持DMA搬运转换结果这是多通道采集不占用CPU的核心。转换结果寄存器分单个通道数据寄存器和组合数据寄存器多通道DMA时通常读组合数据寄存器。DAC侧有独立的DHR数据保持寄存器支持8位、12位左对齐、12位右对齐几种写入方式也支持定时器触发和DMA搬运。这些特性在工控场景里的对应关系是压力变送器、温度变送器的4-20mA信号经采样电阻转成电压后接ADCDAC输出0-10V或者4-20mA信号去控制变频器、电动阀。我的项目里用了4路ADC输入2路DAC输出基本上把片内外设用到了七成。1.3 项目对驱动提出哪些硬指标拿到需求别急着写代码先定指标。我这次项目的硬指标如下大家可以根据自己的场景调整ADC采样率做到1kHz4个通道循环扫描每个通道都能达到这个刷新率。多通道采集必须要用DMACPU只在中途处理数据不能死等。ADC数值要带滤波和标定直接把工程单位输出给应用层不在别的线程里再算一遍。DAC输出一路0-10V斜坡电压一路正弦波频率不要求特别高但波形要连续平滑不能有台阶感。必须适配RT-Thread设备框架应用层通过rt_adc_read和rt_dac_write这类接口访问不允许裸机while轮询。这些指标定完后面的驱动设计思路其实就被框住了。很多新手一上来就查寄存器配置配置完发现上层用起来跟屎一样就是因为没先想清楚接口长什么样。2. 动手前先把RT-Thread工程和硬件测试台准备好2.1 工程搭建RT-Thread Studio创建BSP时钟树先确认我用RT-Thread Studio创建项目时直接选择了对应的GD32H759 BSP。如果你的RT-Thread Studio版本里还没有这块芯片的BSP办法也简单先按照官方例程或者芯片厂商提供的初始化代码把工程基础打好再把RT-Thread内核和FinSH移植进去。BSP能用的前提下我建议先把工程编译下载跑通一个串口打印再开始动ADC/DAC。时钟树是第一个容易翻车的地方。ADC和DAC挂在不同APB总线上BSP默认配置的主频不一定适合模拟外设。我遇到过DMA一直不工作最后发现是DMA对应的总线时钟没开。排查了半天最后还是回到时钟配置里一个个勾选才解决。所以建完工程第一件事把board.c里的系统时钟配置打开确认AHB/APB1/APB2的分频关系再用get_clocks()之类的接口把实际时钟频率打印出来。ADC的采样时钟是从APB2分频出来的分频系数直接决定转换周期上限这个后面配置采样时间时要用到。刚开始调GPIO时还要注意ADR引脚、VREF引脚是否在板子上已经接好。有的开发板把VREF直接接到了3.3V问题不大有的板子预留了外部基准芯片的焊盘默认没焊接这种情况下ADC参考电压就是悬空的读数肯定不对。这个属于硬件问题后面排查漂移时也有可能踩到。2.2 硬件连接与信号源没有这个测试台后续调驱动就是盲人摸象很多人驱动写不好是因为没有稳定的信号源就开调。我见过一位同事ADC值跳来跳去他以为是驱动问题结果是手捏着杜邦线在测。模拟量调试硬件测试台必须搭稳。我的测试环境很简单一个精密可调电源或者信号发生器输出0到3.3V的稳定直流电压接到ADC输入引脚。一个10kΩ电位器跨接在VREF和GND之间抽头接ADC输入用来模拟缓慢变化的传感器信号。DAC输出接示波器探头开始的时候不接任何负载先看空载波形后面再串一个250Ω电阻模拟工控电流环负载。用万用表实时量ADC输入引脚电压和程序打印出来的原始码值对比判断转换结果是否正常。串口通过USB转TTL模块接电脑RT-Thread FinSH控制台负责打印数据和执行命令。特别提醒一点ADC输入走线尽量短如果用的是杜邦线要把线拧在一起减少环路面积否则周围开关电源产生的电磁干扰都会串进去。我第一次用长跳线测高阻抗信号时ADC刚上电数值就开始漂后来把线剪短、加了一个0.1uF对地电容才稳定下来。2.3 为什么我不直接调用库函数而是先做寄存器映射验证RT-Thread BSP里可能带了GD32的外设库调用库函数确实方便但库函数封装得太厚出了问题很难判断是配置顺序错了还是硬件有问题。我在正式封装驱动前通常先写一段最原始的寄存器操作代码只做一件事让ADC用软件触发采一个通道把原始值读回来。这样做的目的有三个第一确认寄存器基地址、时钟门控、GPIO复用都是通的第二验证硬件上引脚到底有没有焊错、VREF有没有电压第三给后续封装设备驱动打底至少知道最底层操作是什么样。这段代码可能只有二三十行但价值很高。rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_ADC0); gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_5); adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_5, ADC_SAMPLETIME_15); adc_resolution_config(ADC0, ADC_RESOLUTION_12B); adc_enable(ADC0); adc_software_trigger_enable(ADC0); while (RESET adc_flag_get(ADC0, ADC_FLAG_END)) { /* wait */ } uint16_t raw_value adc_regular_data_read(ADC0);这段代码看起来简单但它把所有关键配置都串起来了。如果读回来的值跟着电位器转动而变化说明通路OK后面可以放心封装。如果值不变再逐项排查GPIO、时钟、参考电压比直接用库函数省太多时间。库函数版本不同函数名可能稍有差异但底层寄存器逻辑一致关键是理解配置顺序。3. ADC驱动开发寄存器配置、多通道DMA采样与数据滤波3.1 ADC初始化时的关键参数选择逻辑很多初学者把ADC初始化和GPIO初始化差不多随便设一个采样时间就完事。实际上采样时间的选择直接影响转换精度尤其当信号源内阻比较大的时候。ADC转换的第一步是采样保持开关闭合让内部采样电容充电。如果采样时间太短电容还没充到和输入信号一致的电平就断开了转换结果自然偏低。这个现象就像用一根细水管往水桶里接水时间不够水桶没满就称重结果肯定偏小。具体选择多大采样时间要查芯片手册里的采样电容值和输入等效阻抗。我给一个经验公式供参考T_sample (R_source R_switch) × C_sample × ln(2^N)这里R_source是信号源内阻R_switch是内部开关电阻C_sample是采样电容N是分辨率位数。比如12位分辨率下如果信号源内阻是10kΩ采样时间要比低内阻信号明显加长。我的项目里ADC输入端外接了电阻分压网络等效内阻大约几kΩ选了最长的采样时间档牺牲一点转换速度换来稳定读数。另外ADC的时钟频率也不能拉太高。GD32 ADC的时钟一般由APB2分频得到分频系数如果太小转换结果线性度会变差。这个参数没有绝对标准我在实测中发现分频后ADC时钟在30MHz附近比较稳定再高就容易出现跳字。3.2 多通道规则组DMA的配置方法与代码骨架多通道采集的核心思路是配置一个规则组把要采的几个通道排队然后用DMA把每一次转换结果搬到内存数组里。DMA跑起来之后CPU完全不用管转换时序只在DMA半满或全满中断时取数据。配置DMA时有几个细节很容易踩坑。第一要确认ADC0对应的是哪一个DMA控制器和通道这个必须查芯片参考手册不能照搬STM32的映射表GD32虽然参考了ST的架构但DMA请求映射不完全一样。第二DMA缓冲区要和DMA传输宽度匹配ADC是16位数据内存数组必须是uint16_t不要定义成uint32_t。第三循环模式下缓冲区长度和通道数量之间的关系要算清楚。下面是我调通过的核心配置代码骨架#define ADC_CH_NUM 4 #define ADC_DMA_BUF_SZ (ADC_CH_NUM * 8) /* 每个通道留8个缓冲位 */ uint16_t adc_dma_buf[ADC_DMA_BUF_SZ]; dma_parameter_struct dma_p; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH0); dma_struct_para_init(dma_p); dma_p.request DMA_REQUEST_ADC0; dma_p.direction DMA_PERIPHERAL_TO_MEMORY; dma_p.memory_addr (uint32_t)adc_dma_buf; dma_p.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_p.memory_width DMA_MEMORY_WIDTH_16BIT; dma_p.periph_addr (uint32_t)ADC_CDATA(ADC0); dma_p.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_p.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_p.number ADC_DMA_BUF_SZ; dma_p.priority DMA_PRIORITY_HIGH; dma_circulation_enable(DMA0, DMA_CH0); dma_parameter_init(DMA0, DMA_CH0, dma_p); adc_dma_mode_enable(ADC0); dma_channel_enable(DMA0, DMA_CH0);这里特别注意periph_addr我用的组合数据寄存器ADC_CDATA因为在多通道规则组扫描模式下DMA每搬运一次数据数据寄存器内容就是当前通道的转换结果。如果只用一个固定的ADC_RDATA多通道时拿到的永远是最后一次转换的值程序里就乱了。DMA缓冲区长度我固定为4 × 8也就是每通道8个点。为什么是8因为我要在中断里做简单的滑动滤波缓冲区太少滤波窗口不够太多则DMA中断频率太低实时性差。这个值需要根据采样率和中断负载trade-off后面调参时再细说。3.3 滤波与标定原始码值怎么变成工程单位DMA搬运回来的原始值直接塞给应用层肯定不合适。一方面会有随机噪声另一方面不同信号调理电路的零点和满量程不完全一致。所以我在驱动里加了两层处理软件滤波和线性标定。软件滤波最简单有效的是滑动平均。我一般先去掉最大最小值再做平均避免脉冲干扰对结果影响太大。代码不复杂#define FLT_WINDOW_SIZE 8 uint16_t adc_filtered(uint16_t *buf, uint32_t len) { uint32_t sum 0; uint16_t min 0xFFFF; uint16_t max 0; for (uint32_t i 0; i len; i) { if (buf[i] min) min buf[i]; if (buf[i] max) max buf[i]; sum buf[i]; } sum - min max; return (uint16_t)(sum / (len - 2)); }这个函数假设窗口长度大于等于3去掉一个最大值和一个最小值后取平均。用在工控场合对付传感器本身的随机噪声够了。如果干扰是有规律的工频噪声光靠滑动平均压不下去就得考虑加陷波或一阶低通float adc_lpf_value 0.0f; #define ADC_LPF_ALPHA 0.2f void adc_lpf_update(uint16_t raw) { adc_lpf_value (1.0f - ADC_LPF_ALPHA) * adc_lpf_value ADC_LPF_ALPHA * (float)raw; }一阶低通系数需要根据采样频率和希望的截止频率来算不能随便拍脑袋。ALPHA越小滤波越平滑但响应越慢工控上压力和温度本身就是慢变量ALPHA取0.2问题不大。标定公式也很直接。如果12位右对齐参考电压是3.3V那么原始码值到电压的换算就是voltage_mv (int32_t)raw * 3300 / 4096如果是4-20mA电流环外部采样电阻250Ω则电压1V对应电流4mA5V对应20mA。换算成电流current_mA (voltage_mv / 1000.0f) / 250.0f * 1000.0f简化后就是current_mA voltage_mv / 250.0f。这个公式看起来简单但非常重要因为它在应用层把原始码值和物理量彻底解耦。后面即使换了250Ω采样电阻以外的电阻也只需要改一个常数不用动上层逻辑。3.4 一个容易忽视的坑DMA缓冲要处理D-Cache一致性GD32H759用的是Cortex-M7内核带D-Cache。如果RT-Thread启动时把D-Cache打开了DMA把数据从外设搬到内存后CPU读到的可能是Cache里的旧数据而不是DMA刚写入的新数据。这个坑很隐蔽现象就是ADC数据卡住或者隔一会更新一次而且每次更新跳变一大截。解决办法有两个。最简单的是把DMA缓冲区放到非Cache区域比如定义到__attribute__((section(noncache)))对应的内存段或者在RT-Thread里用rt_dma_alloc之类的接口分配。另一个办法是每次读取前做无效化SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, sizeof(adc_dma_buf));注意这个函数的地址参数要求32字节对齐缓冲区定义时要用RT_ALIGN(..., 32)对齐否则可能在边界上出问题。我在第一篇笔记里专门讲过Cache一致性问题ADC/DMA驱动里它同样重要没处理好的话后面所有滤波都是对着一堆过期数据在做算术白忙一场。4. DAC驱动开发DHR寄存器、定时器触发与波形输出4.1 DAC初始化与输出缓冲的取舍DAC比ADC说起来要简单一些但寄存器层面的细节也不少。GD32的DAC模块里写数据不是直接写到转换核心而是先写入DHR数据保持寄存器经过内部逻辑再送出去。为什么要有这一步因为DHR寄存器可以支持不同位宽、不同对齐方式还方便DMA和定时器在合适的时机更新数据。实际初始化代码如下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_OUT0, DAC_TRIGGER_SOFTWARE); dac_output_buffer_enable(DAC0, DAC_OUT0); dac_enable(DAC0, DAC_OUT0); dac_data_set(DAC0, DAC_OUT0, DAC_ALIGN_12B_R, 0x800);这里有个选择输出缓冲到底是打开还是关闭。打开输出缓冲DAC带载能力强可以直接输出到后级电路引脚输出阻抗低。但坏处是内部缓冲放大器会引入失调和增益误差精度要求高时可能不满意。关闭输出缓冲输出阻抗很高必须外接运放否则一接负载电压就掉。我的实际建议是如果DAC后面肯定要接运放或V/I转换电路就把内部缓冲关掉让DAC裸奔精度更好如果只是调试阶段直接接示波器看个波形把缓冲打开图省事。项目里我DAC0接的是外部V/I转换芯片所以内部缓冲关掉了DAC1直接输出0-10V分压信号缓冲打开。4.2 用定时器触发DMA生成可控波形纯软件在循环里改DHR寄存器也能输出波形但有两个问题CPU占用率高而且波形周期受线程调度影响频率不稳。工控里如果用DAC输出一个斜坡电压控制电动阀斜坡抖动会导致执行机构一卡一卡的。所以项目最终用了定时器触发DMA搬运的方式。思路很简单预先准备一个样本数组用定时器产生固定频率的触发事件每次触发就把DMA把下一个样本值写到DAC的DHR寄存器。这样DAC输出的每一个台阶严格等间隔波形完全由样本数组决定CPU零负担。配置分三步。第一步配置定时器的更新事件作为DAC触发源第二步配置DAC触发源为定时器触发第三步配置DMA从内存到DHR寄存器的循环搬运。/* 伪代码示意具体寄存器请按参考手册配置 */ timer_trigger_output_config(TIMER1, TIMER_TRGO_UPDATE); dac_trigger_source_config(DAC0, DAC_OUT0, DAC_TRIGGER_TIMER1); dac_trigger_enable(DAC0, DAC_OUT0); /* DMA: memory - DAC_DHR12R0 */ dma_p.direction DMA_MEMORY_TO_PERIPHERAL; dma_p.periph_addr (uint32_t)DAC_DHR12R0(DAC0, DAC_OUT0); dma_p.memory_addr (uint32_t)waveform_table; dma_p.number WAVEFORM_TABLE_SIZE; dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0);输出正弦波时要提前算好样本表。比如我生成64个点的正弦波#define TABLE_SIZE 64 uint16_t waveform_table[TABLE_SIZE]; for (int i 0; i TABLE_SIZE; i) { float v 2048.0f 2048.0f * sinf(2.0f * 3.1415926f * i / TABLE_SIZE); waveform_table[i] (uint16_t)v; }触发频率和波形频率的关系是f_wave f_trigger / TABLE_SIZE。如果定时器触发频率是6.4kHz64个点输出波形就是100Hz。想改频率可以改触发频率或者改样本点数。注意样本值如果是12位右对齐范围是0到4095正弦波以2048为中心幅度2048相当于接近满偏但不削顶。4.3 DAC输出精度和噪声的处理DAC输出看起来简单但实际测量时很容易发现毛刺和噪声。我用示波器看DAC输出正弦波时发现波形叠加了高频噪声幅度大概几毫伏频率正好是板子上DC-DC开关频率。这其实是PCB布局和电源噪声问题。处理办法主要在硬件三方面这也是不少同行总结出来的经验我按优先级排序:模拟电源和数字电源分开供电ADC/DAC的供电引脚用LC滤波单独供给VREF用专门的基准芯片不要直接拿数字3.3V。模拟地和数字地单点汇接尽量保证ADC/DAC下方地平面完整不要让数字信号的返回电流穿过模拟区域。DAC输出走线远离SPI、PWM、时钟等快速翻转信号线输出引脚加一个RC低通比如100Ω串阻加1nF电容到地截止频率大约1.6MHz能抑制大部分高频毛刺。软件侧也可以做一点事。很多人提DAC插值数字滤波器MCU内置DAC本身没有这个硬件但可以在两个输出样本之间插入线性插值点相当于软件把输出更新频率提高。比如原来64个点更新一次现在在相邻点中间多产生一个中间点等效128个点。缺点是DMA传输量翻倍但波形平滑度明显改善。我在对正弦波质量要求高的那一路DAC上用了这个办法代价是定时器触发频率跟着翻倍CPU还是零负担只是样本表大一倍。5. 实测中的三个典型雷区与完整排查链路5.1 雷区一ADC多通道扫描时第一个数据总是不对这个问题是我在多通道DMA调试到第三天时遇到的。现象很稳定系统上电后DMA跑起来4个通道的数据里第一个通道的第一个数据明显偏大后面每个周期的第一个数据又正常了。也就是说只有每次DMA启动后的第一次转换有问题。我最初怀疑是DMA缓冲区初始化有问题把数组清成0也还是这样。后来想到规则组扫描时第一个通道是从初始化状态开始的采样保持电容可能残留了上一个通道的电压尤其第一个通道前面根本没有上一个通道状态不确定。解决的方法是在DMA启动之前先禁用DMA传输软件触发一次完整的规则组转换把这次转换的数据读走丢弃然后再使能DMA。这样等到DMA真正开始搬运时采样保持电路已经稳定在工作状态了。代码里就是在adc_software_trigger_enable后等一下ADC_FLAG_END再清标志然后开DMA。这个经验其实也能扩展到STM32H7上规则组扫描的第一次转换有偏差是常见现象不能忽略。当时如果直接上多通道DMA而不做这一步整个系统的零点就会偏后面标定怎么标都别扭。5.2 雷区二DAC输出带不动4-20mA负载电压被拉垮调试DAC输出时我把示波器接到DAC引脚上空载波形很漂亮正弦波对称、幅度准确。然后我随手把DAC输出接了一个250Ω电阻到地再一看波形整个幅度塌了一半。一开始我以为是把内部输出缓冲关了导致的于是重新打开缓冲还是有压降只是好一点点。后来翻数据手册才发现MCU内部DAC的输出缓冲驱动能力很弱它的设计目的是把输出阻抗降低到几十kΩ级别方便后级放大而不是直接驱动毫安级负载。250Ω电阻需要至少几毫安电流内部缓冲根本给不了。这个问题不是芯片缺陷而是没有为DAC设计正确的输出级。工控上的4-20mA输出标准做法是DAC输出接精密运放再通过V/I转换电路或者直接用专用的电流环芯片。我在硬件上改了方案DAC引脚接了一个轨到轨运放跟随运放输出再接V/I转换问题立刻消失。排查过程中的一个教训是不要一看到DAC输出幅度不对就怀疑寄存器配错了先算一下负载需要的电流再算一下DAC能提供多少电流这两个数字对比就能定位一大半问题。5.3 雷区三ADC数值漂移原来是我在RT-Thread里频繁开关中断搞得DMA半满标志丢了一半第三个坑最有意思现象也最隐蔽。系统跑了一段时间后ADC采集值整体慢慢往上漂然后每隔几十毫秒又跳回正常值周而复始。我第一反应是参考电压漂了拿万用表量VREF纹波很小排除然后量VDDA也稳接着怀疑ADC采样时间不够把采样时间调到最长现象依旧。最后静下来梳理代码发现DMA中断处理函数里我用rt_enter_critical()包了一段比较长的数据处理流程目的是保护环形缓冲区。但GD32的DMA半满中断是事件型的如果上一次半满事件还没来得及清除下一次DMA又到了半满那么这个半满标志很可能被覆盖掉导致我的驱动以为后半段数据还没准备好实际上缓冲区已经被DMA覆盖写入了。问题就出在我把太多处理逻辑放进了临界区。中断处理应该只做最轻量的事情释放信号量、切换缓冲区指针。具体滤波和标定放到线程里做不要关中断关这么久。修改后的处理方式DMA半满中断里只做一件事rt_sem_release(adc_sem)。线程里等待信号量然后拷贝当前半区数据再调用滤波和标定。拷贝前用SCB_InvalidateDCache_by_Addr处理缓存。跑了两天数据稳定问题彻底消失。这个坑也提醒我在RT-Thread里写驱动不能完全照搬裸机风格中断里该给系统让路的时候就得让路。5.4 排查方法论从现象到根因的四步走这三个雷区踩下来我总结了一套模拟量问题的排查流程虽然简单但很管用先确认硬件环境稳定示波器量参考电压、电源纹波、DAC输出负载条件排除外部因素。用固定输入做对照给ADC输入一个精密可调电源的电压如果读数跟着变说明通路正常如果不跟问题在初始化或寄存器。每次只改一个参数改采样时间、改滤波系数、改中断处理方式不要一次性改三四个变量。回归验证要够久模拟量的问题往往是偶发性的修完以后至少要跑几个小时最好过夜观察数据是否还会周期性跳变。这套流程帮我避免了很多瞎调参数的时间浪费。驱动问题90%都可以通过控制变量法快速锁定真正难的是你能不能忍住不拍脑袋。6. 把驱动装进RT-Thread设备框架让应用层调用更安全6.1 为什么不用裸机while轮询而要走设备框架当ADC/DAC底层调通之后下一个问题是应用层怎么访问这些数据在RT-Thread里最自然的方式是注册成标准设备应用层用rt_device_find、rt_adc_read、rt_dac_write这类通用接口来操作。这样做的最大好处是解耦。上层逻辑不需要知道底层跑的是GD32还是STM32只需要拿到设备句柄调接口。另一个好处是方便调试。RT-Thread的FinSH命令可以直接读取设备状态我在调试时用一条命令就能把ADC原始值和换算后的电压同时打出来不用反复改代码重新编译。6.2 ADC/DAC设备ops实现与注册示例我参考RT-Thread内核里已有的ADC设备驱动模板实现了几个ops。下面是一个简化版的ADC设备注册例子核心是把之前的底层配置和读取通过ops暴露出去。static rt_err_t drv_adc_enabled(struct rt_adc_device *dev, rt_uint32_t channel, rt_bool_t enabled) { /* 打开或关闭某个通道内部配置对应的规则组 */ return RT_EOK; } static rt_err_t drv_adc_read(struct rt_adc_device *dev, rt_uint32_t channel, rt_uint32_t *value) { /* 从DMA缓冲区中取该通道最新的一次原始值 */ *value adc_latest_values[channel]; return RT_EOK; } static struct rt_adc_ops drv_adc_ops { .enabled drv_adc_enabled, .read drv_adc_read, }; static struct rt_adc_device adc0_dev; int rt_hw_adc_init(void) { /* 底层ADC和DMA初始化 */ gd32_adc_dma_init(); adc0_dev.parent.user_data adc0_handle; rt_device_adc_register(adc0_dev, adc0, drv_adc_ops); return RT_EOK; }DAC侧同理注册成dac0提供write和enabled接口。这里不展开所有代码重点是思路设备框架只负责接口规范化真正的寄存器操作还是复用我们前面验证过的那一套底层函数。注意不同RT-Thread版本里rt_adc_ops结构体定义可能略有差异以你使用的版本头文件为准。6.3 多线程下的数据同步与内存一致性设备框架调通之后还面临多线程访问的问题。ADC DMA持续在跑应用线程随时可能来读数据。如果读的时候DMA正在写同一个缓冲区读到的值可能是一半旧一半新造成数据撕裂。我的处理办法是双缓冲加信号量。DMA缓冲区分成AB两个半区DMA半满中断表示A半区写完全满中断表示B半区写完。中断里只更新一个标志并释放信号量线程拿到信号量后先确定当前应该读哪个半区然后把这个半区的数据拷贝到本地再处理。这样DMA写另一个半区时读写互不干扰。内存一致性上缓冲区定义为32字节对齐的uint16_t数组读取前调用Invalidate。RT-Thread里可以用这样的方式声明static uint16_t adc_dma_buf[ADC_DMA_BUF_SZ] __attribute__((section(noncache), aligned(32)));所以最终设备的read接口返回的不是DMA缓冲区地址而是拷贝出来的稳定数据副本。牺牲一点内存换来的是一次接口调用拿到的数据一定是完整的这个取舍很值。7. 折腾一年后的几句实在话7.1 那些写在代码注释里的教训这个项目从原型到产线前后折腾了一年。最深的体会是模拟量驱动调试硬件和软件不能分家。很多ADC漂移问题到最后都是布局、地平面、参考电压的问题。软件滤波可以掩盖一部分问题但它只是遮羞布硬件不解决换个电磁环境更恶劣的现场数据还是会漂。如果让我给刚开始做GD32H759模拟量驱动的工程师三个建议我会说先把单通道采集调通再上DMA先用示波器确认硬件输出再写滤波算法每次调试只改一个参数别急着怀疑库函数有问题。这三个原则帮我避开了绝大多数无意义的内耗。7.2 后面我打算怎么扩展这套驱动这套ADC/DAC驱动已经稳定运行下一步我想把DAC输出和ADC采集做成一个简易的闭环。具体来说用定时器触发DAC生成一个扫描电压同时采样回读在RT-Thread里跑一个简单的PID控制线程根据回读误差修正DAC输出。这样整个模拟量链路就变成了一个完整的控制回路再往上挂触摸屏或者远程IO就非常顺手。另外GD32的多ADC模块还可以做同步采样两个ADC同时采集同一时刻的电压和电流用来做功率计算。这个功能我准备在下一版驱动里加进去接口上尽量保持兼容让应用层无感知切换。驱动开发就是这样把底层细节磨平之后上面跑什么逻辑都轻松很多。
返回列表