ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 DAC正弦波与AD同步采集,Matlab实时绘图完整实现

STM32 DAC正弦波与AD同步采集,Matlab实时绘图完整实现 简介面向STM32与MATLAB联合开发的嵌入式实战资料包围绕数模转换模块连续输出正弦波、模数转换同步采集以及上位机实时绘图展开适合需要学习ARM单片机模拟外设、直接存储器访问与串口通信的开发者与硬件工程师。资源共261个文件压缩包约10.53MB包含C与H源码文件、Keil工程文件、hex/axf/o编译产物、MATLAB脚本、文本说明及调试配置文件不同格式对应源码阅读、固件烧写、数据分析和二次移植等不同使用场景。已有1045人学习下载整套内容可串联起正弦波查表生成、直接存储器访问循环发送、模数采样配置、串口或串行总线通信和MATLAB实时绘图整个实验链路对理解嵌入式模拟信号处理和数据可视化有直接帮助。文件类型虽多但目录结构清晰适合按模块逐层拆解是配套课程设计或竞赛作品时的可复用参考工程。1. STM32 DA 模块连续正弦波与 AD 采集、Matlab 实时绘图到底难在哪多数人第一次看到“STM32 DA模块连续输出正弦波信号与AD采集并配合Matlab实时绘图”这个题目会以为难点是正弦波生成。真做起来恰恰相反正弦波用查表加 DMA 很快就能跑通真正的坑在后面——DA 输出更新频率、AD 采集触发时刻、串口数据帧和 Matlab 绘图循环必须串成一条确定性的数据链。时序稍微错位屏幕上看到的就是一条会“漂”的乱波。这篇文章把链路按 DA 输出、AD 同步采集、串口上传、Matlab 实时绘图四段拆开每段给出必调参数和可复现代码。适合正在做波形发生器、阻抗测量或电机控制前级激励的工程师也适合拿这块板子做毕业设计的人。后面所有配置以 STM32F1/F4 的通用外设和 STM32Cube HAL 为例其他系列只需对照时钟树和 DMA 请求号平移。2. STM32 DA 模块连续输出正弦波信号查表、定时器与 DMA 参数2.1 为什么首选“正弦查表 定时器触发 DMA 循环”直接在主循环里调用HAL_DAC_SetValue扫表CPU 占用和时间抖动都会让波形变脏定时器中断里逐点写 DAC 也一样中断响应延迟会导致更新沿不整齐。常见做法是用一个定时器的 TRGO 事件触发 DAC 转换再用 DMA 把正弦查找表按循环模式搬进 DAC 数据寄存器。这种方案有两个优点一是 CPU 完全不参与每个点的搬运DAC 更新频率只由定时器决定二是「连续」有硬件保证DMA 循环模式搬完表最后一个点后会自动回到表头中间没有一条指令缝隙。正弦表点数 N 直接决定波形质量N 越大DAC 输出的阶梯越小但同一触发频率下正弦波频率会按 1/N 下降。比如定时器触发频率 1MHzN128 时正弦波只有 7.8125kHzN256 时降到 3.90625kHz。2.2 用 CubeMX 配置 DA 模块、定时器和 DMA 的最小参数下面是一份能跑通的最小配置不是在列“唯一答案”而是照这些值填基本能避开“DAC 不输出”的问题。定时器选择上没有硬性要求F1/F4 上挑一个没被占用的通用定时器即可我这里用 TIM2。配置位置推荐值说明DAC 输出缓冲Enable驱动能力更强外接高阻负载或示波器不会明显塌陷DAC 触发源Timer 2 Trigger Out event由 TIM2 更新事件触发 DAC 取下一个点DMA ModeCircular循环搬运保证波形不中断DMA DirectionMemory To Peripheral从正弦表搬到 DAC 数据寄存器DMA Memory Address IncrementEnable每搬完一个点内存地址前进一个元素DMA Peripheral Address IncrementDisableDAC 寄存器地址固定DMA Data WidthHalf Word12 位 DAC 数据用 uint16_t 承载TIM2 Clock SourceInternal Clock直接使用 APB 定时器时钟TIM2 Prescaler0不分频方便算频率TIM2 Period7172MHz / (01) / (711) 1MHz 触发TIM2 Trigger Output (TRGO)Update Event供 DAC 做外部触发其中最容易漏的是 TIM2 的 TRGO 没改成 Update Event。CubeMX 里 Timer 配置页默认 TRGO 可能是 Disabled不改的话 DAC 得不到触发脉冲输出一直停在 0V 附近。2.3 生成正弦查找表与 STM32 启动代码正弦表不需要手算下面这段 Python 生成 128 点、12 位右对齐的表import math N 128 # 每周期点数 mid 2047.5 # 12位DAC中点范围0~4095 amp 2047.5 # 峰值幅度不顶到0/4095 for i in range(N): v int(round(mid amp * math.sin(2 * math.pi * i / N))) print(f0x{v:04X},, end) if (i 1) % 8 0: print()逻辑不复杂DAC 是 12 位输出范围 0~4095中心点取 2048 附近正弦波从 0 到峰值再回到 0不会出现负值。幅度如果想留余量把amp改成比如 1500后续 AD 采集时也不容易削顶。STM32 端启动代码只有三行是核心/* sine_table[128] 由上方脚本生成 */ static uint16_t sine_table[128] { 0x0800, 0x0863, 0x08C6, /* ... */ }; HAL_TIM_Base_Start(htim2); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)sine_table, sizeof(sine_table) / sizeof(sine_table[0]), DAC_ALIGN_12B_R);参数说明DAC_ALIGN_12B_R表示把 uint16_t 的低 12 位写到 DAC 右对齐数据寄存器DMA 外设地址固定为 DAC 的数据保持寄存器内存地址自动递增。sizeof表达式只是为了让你不用手动写点数实际等价于 128。如果你的 MCU 带 D-Cache比如 STM32H7sine_table放在 D-Cache 管理的内存区域时还要做 Cache 维护F1/F4 不需要。频率计算公式是f_trigger TIM_CLK / ((PSC1) * (ARR1)) f_sine f_trigger / N72MHz 主频、PSC0、ARR71 时f_trigger1MHzN128正弦波正好 7812.5Hz。想做其他频率先定 N再用公式反推 ARR。N 做波形观察取 128 就够如果后续要观察谐波再提到 256 或 512。3. 让 AD 采集与 DA 输出共用一个定时器触发同步、采样时间和 DMA 缓冲3.1 为什么 AD 采集要跟 DA 输出共用同一个触发源如果让 ADC 自由运行也就是 Continuous Conversion 一直开采集点相对正弦波的相位是随机的Matlab 里画出来会看到波形左右“游动”。正确做法是让 ADC 也由同一个定时器 TRGO 触发更新。这样每个定时器事件同时给 DAC 和 ADC 一个同步信号DAC 输出表里的第 i 个点ADC 在同一个时间基点上采集后续每一帧数据都能一一对应起来。在 CubeMX 里DAC 触发源选 Timer 2 Trigger Out eventADC 的 External Trigger Conversion Source 也选 Timer 2 Trigger Out event。不是用两个定时器去凑时间而是两个外设直接共用一条触发线。这样 MCU 内部硬件省去了“软件对齐”这一步波形起始相位、AD 采到的点序都是确定的。3.2 ADC 配置参数与采样时间计算AD 采集这里有一组推荐参数和自由跑 ADC 的习惯完全不同ADC 参数推荐值说明Resolution12-bit与 DAC 位数对齐直接看原始码值Continuous ConversionDisabled每次由定时器触发不连续自跑Scan ConversionDisabled只采一个通道不需要扫描External Trigger SourceTimer 2 Trigger Out event与 DAC 共用触发源Sampling Time13.5 Cycles对低频正弦足够稳定余量也大DMA ModeCircularADC 结果不断搬进内存不占用 CPUDMA DirectionPeripheral To Memory外设是 ADC 数据寄存器DMA Data WidthHalf WordADC 12 位结果放到 uint16_t采样时间不能随便拉满要算一下单次转换时间。STM32 的 12 位 ADC单次转换时间大约是t_conv (采样周期 12.5) / ADC时钟频率假设 ADC 时钟 12MHz采样时间 13.5 Cycles那么 t_conv 26 / 12MHz ≈ 2.17us。只要定时器触发周期比它长就不会出现上一次转换还没完成、下一次触发已经到来的情况。把定时器触发频率控制在 50kHz也就是周期 20us余量很足。相比之下前面 DAC 示波场景里的 1MHz 触发对普通 ADC 来说太快做完整 AD 采集链路时我一般先降回 50kHz。3.3 AD 的 DMA 缓冲与起始相位对齐下面这段代码把 ADC DMA 缓冲设为 256 个点对应 N128 的两个完整正弦周期#define DAC_N 128 #define ADC_FRAME_LEN (DAC_N * 2) /* 256个点两个整周期 */ static uint16_t adc_buf[ADC_FRAME_LEN]; void start_sine_and_adc(void) { /* 先停定时器避免配置过程中产生乱触发 */ HAL_TIM_Base_Stop(htim2); HAL_ADC_Start_DMA(hadc, (uint32_t *)adc_buf, ADC_FRAME_LEN); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)sine_table, DAC_N, DAC_ALIGN_12B_R); /* 再启动定时器DAC 和 ADC 同时开始工作 */ HAL_TIM_Base_Start(htim2); }顺序是关键先停定时器再启动 ADC DMA 和 DAC DMA最后开定时器。如果反过来定时器已经跑了ADC DMA 还没准备好前几个触发会丢失Matlab 首帧会有一段残缺波。adc_buf长度取正弦表点数的整数倍是为了让每一帧数据都从同一个相位开始Matlab 拼接时不会出现周期跳变。ADC DMA 搬完一整帧后会触发HAL_ADC_ConvCpltCallback。实际工程里应该在这里置一个标志位主循环看到标志再发串口不要去读还没有写完的adc_buf。如果想连续发送不丢帧就用双缓冲一个缓冲给 DMA另一个缓冲在串口发送两个角色轮流切换。4. STM32 到 Matlab 实时绘图的数据通路二进制数据帧、校验和串口波特率4.1 为什么不用文本行传输而用二进制帧不少人习惯用printf(%d\n, adc_val)打印Matlab 里再用str2num或sscanf解析。几百个点做演示没问题但 ADC 一帧 256 点、一秒 195 帧时文本转换会浪费大量时间而且十进制字符串比原始字节多占一倍空间。常见做法是发二进制帧ADC 值按 uint16_t 小端字节序发出去帧头固定长度明确Matlab 直接按字节读不需要做字符串拆分。二进制帧还能顺便带校验检测到 USB 转串口丢字节时能主动丢弃坏帧而不是把错数据画成跳变尖峰。4.2 AD 数据帧格式与 STM32 发送实现我建议的帧格式很简单一共四段字段长度说明帧头2 字节固定 0xAA 0x55用于对同步样本数 n2 字节uint16 小端本次 ADC 数据点数ADC 数据2*n 字节每个样本 16 位小端校验1 字节帧头到最后一个数据字节的逐字节异或STM32 端发送代码可以写成这样#define ADC_FRAME_LEN (128 * 2) static uint8_t txbuf[2 2 2 * ADC_FRAME_LEN 1]; static volatile uint8_t uart_tx_busy 0; void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { uart_tx_busy 0; } } void send_adc_frame(uint16_t *data, uint16_t n) { while (uart_tx_busy) {} /* 工程中建议加超时 */ uint8_t csum 0; uint16_t idx 0; txbuf[idx] 0xAA; txbuf[idx] 0x55; txbuf[idx] n 0xFF; txbuf[idx] (n 8) 0xFF; csum 0xAA ^ 0x55 ^ txbuf[2] ^ txbuf[3]; for (uint16_t i 0; i n; i) { uint8_t lo data[i] 0xFF; uint8_t hi data[i] 8; txbuf[idx] lo; txbuf[idx] hi; csum ^ lo ^ hi; } txbuf[idx] csum; uart_tx_busy 1; HAL_UART_Transmit_DMA(huart, txbuf, idx); }逻辑说明txbuf[3]存长度的高字节csum的初值已经包含帧头和长度两个字节逐字节异或整个数据区后作为帧尾。接受方如果算出来的异或和帧尾不一致说明串口丢字节这一帧应该直接丢弃。while (uart_tx_busy)是确保上一次 DMA 没有发送完之前不覆盖txbuf实际工程要加超时否则 DMA 异常时可能死等。波特率选得多大取决于采样率。用 50kHz 采样率、每点 2 字节、帧头每 256 点占 4 字节实际数据量约 100kByte/s。921600 波特率按 8N1 换算约 115200 Byte/s刚好留出余量。如果采样率翻倍到 100k就必须换 2M 以上波特率或者改用 USB 虚拟串口。4.3 Matlab 端串口的 3 个关键参数Matlab 端用serialport打开串口后先改三个地方s serialport(COM8, 921600); % 波特率必须和 STM32 一致 s.ByteOrder little-endian; % 匹配 STM32 小端字节序 s.InputBufferSize 8192; % 大于一帧长度避免读取时溢出 s.Timeout 2;ByteOrder是文档里容易忽略的参数。STM32 默认小端Matlab 的serialport默认也是 little-endian但如果你改过ByteOrderread(...,uint16)会把字节顺序读反波形变成乱跳。InputBufferSize只需要大于最大帧长不必设得过大8192 对 517 字节的帧已经足够。Timeout设成 2 秒既能容忍串口分段到达又不会在断开时卡死脚本。读取单帧的函数可以这样写function data read_adc_frame(s) while true b1 read(s, 1, uint8); if b1 ~ 0xAA continue; end b2 read(s, 1, uint8); if b2 0x55 break; end end len read(s, 2, uint8); n double(len(1)) 256 * double(len(2)); data read(s, n, uint16); chk read(s, 1, uint8); % 实际使用时可把chk与计算值比对 end这段代码先找帧头 0xAA 0x55再读长度和 n 个 uint16 数据。chk变量读出来后可以根据帧格式里的异或规则验证校验失败就返回空数组或继续读下一帧。演示场景不做状态机也能跑生产级建议把字节流组装改成逐字节状态机避免一帧解析失败后整条流卡死。5. Matlab 实时绘图的三个进阶设置环形缓冲、刷新频率与自检5.1 环形缓冲区加只更新 YData不要在实时绘图循环里反复调用plot重建曲线。正确做法是循环外先建一条线循环里只改YData。显示缓冲区用一维环形数组ST 端来多少数据就写多少画图时只取最后 2048 个点BUF 8192; M 2048; buf zeros(1, BUF); ax gca; h plot(ax, 1:M, zeros(1, M)); ylim(ax, [0 4095]); grid(ax, on); wptr 1; while true d read_adc_frame(s); m numel(d); idx mod(wptr:wptrm-1, BUF); idx(idx 0) BUF; buf(idx) d; wptr mod(wptr m - 1, BUF) 1; sIdx mod(wptr-M:wptr-1, BUF); sIdx(sIdx 0) BUF; set(h, YData, buf(sIdx)); drawnow limitrate; endset(h,YData,...)只更新曲线数据不重建图形对象是实时绘图提升帧率最有效的一招。drawnow limitrate会丢弃来不及刷新的中间帧把刷新率限制在 20fps 左右观察低频正弦足够。如果想更顺滑可以去掉limitrate但 CPU 占用会明显上升。5.2 显示点数与刷新频率怎么配显示段 M 最好取每周期点数 N 的整数倍。N128M2048 就是 16 个完整周期Matlab 端每次看到的相位起点固定做 FFT 时频谱泄漏也更小。M 在 1024 到 4096 之间比较合理再大只会让曲线密集到看不清同时拖慢刷新率。实时绘图里ylim、grid这些命令放在循环外。循环内每帧调用axis auto会触发坐标范围重算开销非常大。如果需要显示 ADC 原始码值直接把YData范围固定在 0~4095观察 DAC 输出加直流偏置后的完整波形时不用反复缩放坐标轴。5.3 用采样率换算时间轴并做 FFT 自检定时器触发频率决定了 ADC 采样率。前面配置里 PSC0、ARR1439、TIM_CLK72MHz那么采样率是 50kHz每两个采样点间隔 20us。Matlab 画时间轴时直接乘以这个值不要用xlim里的点数假装是时间。验证整个链路是否同步最有效的办法是对当前显示段做一次 FFTfs 50000; seg double(buf(sIdx)); seg seg - mean(seg); % 去直流只看信号频率 Y fft(seg); f_axis (0:numel(seg)-1) / numel(seg) * fs; [~, k] max(abs(Y(2:numel(seg)/2))); fprintf(实时频率: %.2f Hz\n, f_axis(k));如果定时器配置是 50kHz 采样、每周期 128 点这里应读到约 390.625Hz。偏了几个 Hz通常不是代码问题而是开发板的 HSE 晶振不是精确 8MHz导致整个定时器时钟和 ADC 采样率一起偏。幅度上ADC 原始码值换算电压是code / 4095 * 3.3直接测 DAC 输出时中点约 1.65V峰峰值接近 3.3V外接负载后波形塌陷就要查 DAC 输出缓冲和负载阻抗了。把 PSC、ARR、N、fs 都定义成一组宏Matlab 端只读配置头以后换采样率不用逐行改绘图程序。本文还有配套的精品资源点击获取
返回列表