常州建设银行网站个人 网站备案
常州建设银行网站,个人 网站备案,室内设计平面图尺寸,至高建设集团 网站1. 从零开始#xff1a;为什么你需要一个高速ADC采集与可视化系统#xff1f;
如果你正在捣鼓STM32#xff0c;尤其是像F405这种带D-M-A#xff08;直接存储器访问#xff09;和高速ADC#xff08;模数转换器#xff09;的芯片#xff0c;那你肯定遇到过这样的烦恼 DMA_HandleTypeDef hdma_adc1;为什么RT-Thread Studio通常只要求复制MspInit函数因为RT-Thread的设备框架理论上会调用标准化的rt_hw_adc_init()。但针对ADCDMA这种高速、非标准的用法框架的支持可能不完善所以我们把完整的初始化都搬过来取得完全的控制权。这是我踩过坑后的经验。4.2 启用外设与配置串口启用宏定义打开工程中的board.h文件确保ADC和UART对应的宏定义是开启的。例如#define BSP_USING_ADC1 #define BSP_USING_UART1配置HAL库检查stm32f4xx_hal_conf.h文件确保以下宏定义已打开#define HAL_ADC_MODULE_ENABLED #define HAL_DMA_MODULE_ENABLED #define HAL_UART_MODULE_ENABLED配置串口参数我们将使用一个串口比如UART1来向上位机发送数据。在RT-Thread的设置文件RT-Thread Settings中确保UART1设备驱动被启用。我们后续会在代码中以更高的波特率如921600或更高来配置串口以确保能跟上2MHz采样产生的数据洪流。计算一下2MHz采样每个点如果是12位数据我们可能用2个字节发送那一秒钟就是4MBytes的数据量串口根本不可能实时传完。所以我们实际是“采集一段发送一段”。例如每次DMA采集2000个点耗时1ms然后花几十毫秒通过串口发送出去。这样对串口压力就小多了也能实现“准实时”观测。完成这些配置工程的骨架就搭好了。接下来就是最核心的编程部分如何让ADC、DMA和RT-Thread的线程协同工作。5. 核心代码实现线程、DMA与信号量的共舞这一部分我们写一个独立的.c文件比如adc_dma_app.c来实现业务逻辑。代码会有点长但我会分段解释清楚。5.1 全局变量与初始化首先定义一些全局变量和初始化函数。#include rtthread.h #include rtdevice.h #include board.h #include adc.h // 包含HAL_ADC_Init等原型 /* 全局ADC和DMA句柄来自复制的代码 */ extern ADC_HandleTypeDef hadc1; extern DMA_HandleTypeDef hdma_adc1; /* DMA搬运的目标缓冲区大小 */ #define ADC_DMA_BUFFER_SIZE 2000 /* 实际存储ADC值的数组 */ static uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE]; /* 用于通知主线程“DMA传输完成”的信号量 */ static struct rt_semaphore dma_complete_sem; /* 串口设备句柄用于发送数据到VOFA */ static rt_device_t uart_dev; /* 初始化ADC和DMA硬件 */ static int adc_dma_hw_init(void) { rt_err_t ret RT_EOK; /* 查找RT-Thread的ADC设备可选这里我们主要用HAL库直接控制 */ // rt_adc_device_t adc_dev (rt_adc_device_t)rt_device_find(adc1); // if (adc_dev RT_NULL) { // LOG_E(ADC device not found!); // return -RT_ERROR; // } /* 调用从CubeMX复制的初始化函数 */ MX_DMA_Init(); MX_ADC1_Init(); HAL_ADC_MspInit(hadc1); // 这个函数内部完成了DMA与ADC的链接(__HAL_LINKDMA) /* 初始化完成信号量 */ rt_sem_init(dma_complete_sem, dma_sem, 0, RT_IPC_FLAG_FIFO); return ret; }这里注意我们没有使用RT-Thread的ADC设备接口rt_adc_read因为它通常用于低速、单次或轮询读取。对于高速DMA传输我们直接操作HAL库函数更直接高效。5.2 DMA传输完成中断与回调函数DMA传输完成中断是协调采集节奏的关键。/* DMA2 Stream0中断服务函数根据你的CubeMX配置调整 */ void DMA2_Stream0_IRQHandler(void) { rt_interrupt_enter(); // 进入中断通知RT-Thread内核 HAL_DMA_IRQHandler(hdma_adc1); // 调用HAL库的通用中断处理函数 rt_interrupt_leave(); // 离开中断 } /* HAL库的ADC转换完成回调函数 * 当DMA搬运完指定数量的数据后HAL库会调用此函数 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { HAL_ADC_Stop_DMA(hadc1); // 停止ADC的DMA请求 rt_sem_release(dma_complete_sem); // 释放信号量通知主线程 // 注意此处千万不要调用LOG_I等打印函数会严重拖慢中断速度 } }HAL_ADC_ConvCpltCallback是一个弱定义函数我们在自己的文件里重新实现它。当DMA正常传输完我们设定的数量2000个后这个函数被调用。在这里我们做两件关键事停止ADC的DMA传输防止继续覆盖缓冲区释放一个信号量。这个信号量就是告诉等待中的主线程“数据准备好了快来处理”5.3 主线程采集-发送的循环现在我们创建一个RT-Thread线程它负责循环执行“启动采集 - 等待完成 - 发送数据”的工作。/* 启动一次固定长度的ADC DMA采集 */ static void adc_start_acquisition(void) { /* 启动ADC并指定DMA搬运的目标地址和长度 */ if (HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buffer, ADC_DMA_BUFFER_SIZE) ! HAL_OK) { LOG_E(Start ADC DMA Failed!); } } /* 用于VOFA的float协议数据打包 */ typedef union { float f_data; uint8_t bytes[4]; } float_pack_t; /* ADC线程入口函数 */ static void adc_thread_entry(void *parameter) { float_pack_t data_pack; uint8_t tx_buffer[8]; // VOFA float协议帧4字节float 4字节帧尾 /* 初始化串口设备以UART1为例 */ uart_dev rt_device_find(uart1); if (uart_dev) { struct serial_configure config RT_SERIAL_CONFIG_DEFAULT; config.baud_rate 921600; // 提高波特率加快发送速度 config.data_bits DATA_BITS_8; config.stop_bits STOP_BITS_1; config.parity PARITY_NONE; rt_device_control(uart_dev, RT_DEVICE_CTRL_CONFIG, config); rt_device_open(uart_dev, RT_DEVICE_FLAG_TX_BLOCKING); // 使用阻塞发送简单 } else { LOG_E(UART1 device not found!); return; } while (1) { /* 1. 清空信号量准备等待新数据 */ rt_sem_control(dma_complete_sem, RT_IPC_CMD_RESET, NULL); /* 2. 启动一次ADC DMA采集 */ adc_start_acquisition(); /* 3. 等待DMA完成信号量超时时间略大于一次采集的理论时间 */ if (rt_sem_take(dma_complete_sem, rt_tick_from_millisecond(2)) RT_EOK) { // 成功获取信号量表示2000个点已采集完毕 // LOG_I(DMA Transfer Complete. First value: %d, adc_dma_buffer[0]); // 调试用正式运行时注释掉 /* 4. 将采集到的数据通过串口发送给VOFA */ for (int i 0; i ADC_DMA_BUFFER_SIZE; i) { // 将ADC原始值0-4095转换为电压值0-3.3V并打包成float data_pack.f_data (float)adc_dma_buffer[i] / 4095.0f * 3.3f; // 填充VOFA float协议帧小端模式 tx_buffer[0] data_pack.bytes[0]; tx_buffer[1] data_pack.bytes[1]; tx_buffer[2] data_pack.bytes[2]; tx_buffer[3] data_pack.bytes[3]; // 帧尾0x00, 0x00, 0x80, 0x7f tx_buffer[4] 0x00; tx_buffer[5] 0x00; tx_buffer[6] 0x80; tx_buffer[7] 0x7f; // 发送一帧数据8字节 rt_device_write(uart_dev, 0, tx_buffer, 8); } /* 5. 可选发送一个特殊值作为帧分隔符方便VOFA区分每次采集的数据块 */ data_pack.f_data -1.0f; // 例如用-1.0作为分隔标志 for (int j 0; j 4; j) tx_buffer[j] data_pack.bytes[j]; for (int k 0; k 10; k) { // 连续发10个分隔符更明显 rt_device_write(uart_dev, 0, tx_buffer, 8); } } else { LOG_W(ADC DMA Timeout!); // 采集超时可能硬件或配置有问题 } /* 6. 短暂延时控制整体循环周期避免CPU占用率100% */ rt_thread_mdelay(10); // 可以根据需要调整例如想每秒采集100次则周期约为10ms } } /* 创建并启动ADC线程 */ int adc_dma_app_init(void) { rt_thread_t tid; if (adc_dma_hw_init() ! RT_EOK) { return -RT_ERROR; } tid rt_thread_create(adc_send, adc_thread_entry, RT_NULL, 2048, // 栈空间可以给大点因为发送数据时局部数组较多 10, // 线程优先级设置为较高 10); // 时间片 if (tid ! RT_NULL) { rt_thread_startup(tid); LOG_I(ADC DMA Application Thread Started!); return RT_EOK; } else { LOG_E(Failed to create ADC thread!); return -RT_ERROR; } } /* 使用RT-Thread的自动初始化机制在系统启动后运行此初始化函数 */ INIT_APP_EXPORT(adc_dma_app_init);这段代码是整个应用的核心。线程在一个无限循环中工作每次先清空信号量然后启动DMA采集。采集过程完全由硬件在后台执行不占用CPU。线程随即在rt_sem_take处挂起等待。大约1ms后2000个点 2MHzDMA完成触发中断并释放信号量线程被唤醒。接着线程用一个for循环将内存adc_dma_buffer中的2000个原始ADC值逐个转换成电压值浮点数并按照VOFA的float协议打包成8字节一帧通过串口发送出去。发送完一批数据后线程延时一小段时间然后开始下一次采集循环。6. VOFA上位机配置与波形分析实战代码在板子上跑起来之后数据就像流水一样通过串口涌向电脑。现在我们需要在VOFA里“接住”这些数据并把它们变成可视化的波形。连接串口打开VOFA在右下角选择你的开发板对应的串口号如COM3设置波特率为921600与代码中一致数据位8停止位1无校验。点击“打开”按钮。选择数据协议在VOFA主界面右侧点击“协议”选项卡。在“接收协议”中选择Float32位单精度浮点数小端。这个协议对应我们代码里发送的格式4字节float 4字节固定帧尾0x00 0x00 0x80 0x7f。配置显示控件从左侧控件箱拖拽一个Waveform波形图到主工作区。在波形图的属性面板中找到Data数据设置。因为我们只发送了一个通道的数据所以Channel通道数填1。关键一步在Index索引设置中由于我们每帧数据是8字节但协议只解析前4字节作为float所以Frame Length帧长度填8Data Offset数据偏移填0。这样VOFA就能正确地从串口流中拆解出一个个浮点数了。观察波形给STM32的ADC输入引脚比如PA3一个信号。你可以先用一个电位器分压出一个可变的直流电压0-3.3V。上电后你应该能在波形图上看到一条稳定的直线其纵坐标值就是你输入的电压。调整电位器直线应该随之上下移动。测试动态信号现在来点刺激的。找一个信号发生器或者用另一个单片机产生PWM经RC滤波成模拟量产生一个1kHz幅值0-3.3V的正弦波或方波接到ADC引脚。由于我们设置的是2MHz采样采集2000个点正好是1ms的数据也就是一个完整的信号周期。你会在VOFA的波形图上看到一个非常光滑、高密度的周期波形。你可以使用VOFA的测量工具光标来测量波形的频率、幅值、上升时间等。几个实用技巧和踩坑点数据流卡顿如果发现波形刷新一顿一顿的可能是串口波特率不够高或者线程中发送数据后没有适当延时让出CPU。可以尝试提高波特率如2000000或者将rt_device_write改为非阻塞模式并检查线程栈空间是否足够。波形错乱检查VOFA的协议帧长度和偏移设置是否正确。最直接的调试方法是使用VOFA的“接收数据”原始hex显示看看收到的字节流是否符合XX XX XX XX 00 00 80 7F的规律。采样率验证在代码里在释放信号量后翻转一个GPIO引脚的电平。用示波器测量这个引脚它的频率就是你整个“采集发送”循环的频率。如果采集2000点耗时1ms发送耗时20ms那么这个GPIO的周期就是21ms左右。这能帮你精确评估系统的实时性。关闭调试信息务必记住在最终测试高速信号时要把所有在中断服务函数HAL_ADC_ConvCpltCallback和高速循环中的LOG_I打印函数注释掉。这些打印函数速度很慢会严重干扰定时导致你根本采不到想要的信号或者数据严重错位。这是我早期调试时最大的一个坑。7. 性能优化与扩展思路当你成功看到稳定的波形后可以思考如何让这个系统更强大、更实用。1. 提高有效数据吞吐率当前的瓶颈在串口发送。发送2000个浮点数需要2000 * 8字节 16000字节。即使以921600波特率约92KB/s发送也需要约174ms这严重限制了系统的实时性每秒只能更新几次波形。优化方案A数据压缩。可以不发送浮点数而是发送原始12位ADC值2字节。在VOFA端通过脚本乘以一个系数还原为电压。这样数据量减半。优化方案B提高波特率。STM32F405的串口在高速时钟下可以支持到好几M的波特率。可以尝试2M、4M甚至更高波特率但要注意USB转串口芯片和电脑端驱动是否支持。优化方案C使用更快的接口。如果板子有USB、SPI或并口可以考虑使用这些高速接口。例如将STM32配置为USB CDC设备虚拟串口速度远超普通UART。2. 实现多通道交替采集STM32F405的ADC支持扫描模式。你可以配置多个通道比如PA3, PA4, PA5在CubeMX的ADC规则组中按顺序添加它们。DMA会依次将每个通道的转换结果搬运到内存数组中。这样你就能同步采集多个信号。在VOFA中只需要配置对应数量的波形图通道并将数据流按顺序对应即可。3. 触发采集模式目前是自由连续采集。你可以引入一个GPIO外部中断或ADC的看门狗阈值中断作为触发信号。当触发条件满足时才启动一次DMA采集。这对于捕捉非周期性的瞬态信号如一个脉冲非常有用。代码逻辑会稍复杂需要结合中断和状态机。4. 在嵌入式端进行初步处理与其把所有原始数据都扔给上位机不如让STM32先做一些预处理。例如在DMA完成中断后先计算这2000个点的最大值、最小值、平均值、RMS有效值或者做一个简单的FFT快速傅里叶变换找出主频。然后只把这些计算结果发送给上位机显示数据量就小了几个数量级可以实现真正的实时监控。把这个系统搭建成功只是第一步。它就像一个强大的工具箱你可以根据具体的项目需求灵活地更换“工具头”。无论是电机控制、音频处理、电源监控还是传感器信号分析这套基于RT-Thread和STM32F405的高速采集框架都能为你提供清晰的“视力”让你真正看清信号世界的细节。