MPS的SAR ADC,MDC97476 谁用过?我!🙋‍我用过!(附带开源信号分析仪)【下集】

接上集(上午发了一半,差点累亖)

调试

搞过硬件的都知道,这个事情就没有你们简单的,即使是数据手册里面写的那么简单:


脆弱瞬间:point_up_2:
用的4 线 SPI。

main() 中加入 ADC 上电后稳定延时(建议 ≥ 200us):

HAL_Delay(1);  // 上电后延时 ≥ 200us,确保 ADC 准备好

main()MX_GPIO_Init() 之后加入:

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);  // 确保 CS 默认高电平

对,这个就是 bug,先拉高,

使用USB CDC 输出替代串口

打开 STM32CubeMX

在 Peripherals 栏中启用:

Connectivity > USB_DEVICE → Communication Device Class (Virtual Port Com)

设置参数:

Mode : Device_Only

Class For FS IP : Communication Device Class (CDC)

会自动启用:USB_OTG_FS ,并使用 PA11 (DM), PA12 (DP)

修改 main.c :替代 fputc

int _write(int file, char *ptr, int len)
{
  while (CDC_Transmit_FS((uint8_t *)ptr, len) == USBD_BUSY)
  {
    HAL_Delay(1);
  }
  return len;
}

批量发送整串字符串 ,而不是一字节一字节发。你可以这样(最终的代码使用了这个):

char usb_buf[64];

while (1)
{
    uint16_t adc_raw = MDC97476_ReadRaw();
    float voltage = MDC97476_ConvertToVoltage(adc_raw, 3.3f);

    int len = sprintf(usb_buf, "ADC = %u, Voltage = %.3f V\r\n", adc_raw, voltage);
    CDC_Transmit_FS((uint8_t*)usb_buf, len);

    HAL_Delay(1000);
}

波特率可随意设置(无效,USB CDC 不依赖波特率);F411 不支持 USB 全速自带 PHY 直接供电 ,需要外接 USB 供电(5V via VBUS)

MX_USB_DEVICE_Init();

即可初始化 USB CDC 设备,HAL 会默认设置虚拟串口的参数为:

波特率:115200(或其他默认值)

数据位:8

停止位:1

校验位:无

如果需要修改,可以修改:

USBD_CDC_LineCodingTypeDef LineCoding = {
    .bitrate = 115200,  // 可改为 921600 等
    .format = 0x00,     // 停止位:1
    .paritytype = 0x00, // 无奇偶校验
    .datatype = 0x08,   // 8位数据
};

因为 USB 的输出和串口不一样,需要加入一个检测:
// 等待 USB 枚举完成

  while (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED)
  {
    HAL_Delay(10);
  }

实际传输速率

USB FS(全速):

实际最大传输速率约为 1 MB/s(8 Mbps) ;一帧最多 64 字节,每毫秒最多传输一次.

USB HS(高速):

实际最大传输速率高达 30-40 MB/s ;512 字节/帧,1ms 多帧传输.

所以比串口高得多,即使你设置 “波特率” 为 9600,USB CDC 实际仍然以几百 KB/s 的速率传输,只是软件兼容层设置为了 9600。

供电 VDD = 3.3V ,存在以下几种模式:

模式 输入类型 范围估计
单端输入 VIN 对 AGND 0 ~ VDD(即 0~3.3V)
差分输入 VIN+ - VIN- ±VREF/2 或 0~VREF 范围
输入钳位电路 内部钳位/外部保护 输入必须在 AGND - 0.3V ~ VDD + 0.3V 内

这里没有后面的钳位电路,而从实际原理图看:使用了 单端输入 (VIN 是单一引脚,没有 VIN+ / VIN-);前级缓冲运放供电为 +7V / -2V ,输出可能为 负电压

自动读取帧格式:

0xAA + ADC低字节 + ADC高字节 + 电压低字节 + 电压高字节 + 0x55

实时绘图:

电压范围 0~3.3V;图像自动刷新

键盘控制:

按下空格 space:开始 / 暂停采集

按下 ESC:退出并关闭文件与串口

自动保存 CSV 日志:

包括原始 ADC 值和电压值(单位:V);保存为 adc_log_年月日_时分秒.csv

Mean Voltage (V)  Min Voltage (V)  Max Voltage (V)  RMS Voltage (V)  \
Value          1.594536              0.0            3.096         1.812197   

       Std Deviation (V)  
Value           0.861112  

时域分析结果:

数据在 0~3.3V 范围内波动(最大值约 3.096V,最小值为 0V)。

波形在 1.6V 附近居中,有一定周期性,可能是某种低频信号(如慢变化电压或模拟波形)。

频域分析结果:

主频率成分集中在低频部分(如 < 10 Hz),整体频谱没有明显尖锐的高频成分,符合慢变电压的特征。

统计结果(基于 1ms 采样间隔):

指标 数值
平均电压 1.594 V
最小电压 0.000 V
最大电压 3.096 V
RMS(有效值) 1.812 V
标准差(波动性) 0.861 V

那就顺便写一下即使信号源未连接或无信号输出,ADC仍然会采集到“漂浮的”随机数据或电平噪声有哪些原因:

原因 1:ADC 输入悬空导致输入端“漂浮”

若 ADC 输入引脚悬空(未接信号源或信号源为高阻态),其电压处于不确定状态,称为“漂浮”。

浮空输入会导致 ADC 采样到环境电磁干扰(EMI)、电源纹波、邻近 IO 的耦合信号、电容充放电残留电压等。

电压值随机波动;数值不稳定、无规律;偶尔出现 0 或满量程,更多是中间电压。

原因 2:模拟前端存在内部偏置或泄漏路径

某些 ADC(MDC97476)内部带有采样保持电路、偏置电流源、电荷注入结构,即使没有外部信号,也可能在输入端形成一定偏置电压或干扰响应;如果使用的是电容耦合、未加下拉电阻,也会导致浮空电压残留。

原因 3:输入引脚高阻抗,容易感应噪声

ADC 通常具有 >1MΩ 的输入阻抗,在未连接低阻抗源时,相当于“天线”,极易感应周围 50Hz / 60Hz 电源噪声、开关电源纹波等;即使加了低通滤波,也不能完全去除这种低频干扰。

原因 4:电源纹波通过内部耦合影响 ADC

ADC 的基准电压或供电电压如果不稳定,特别是在模拟地与数字地耦合较差时,也可能在无信号下采集到虚假的非零电压值

问题 建议
悬空导致不稳定 用 电阻下拉(如10kΩ~100kΩ)至 GND,或者连接一个稳定的信号源/参考电压
电源纹波引起干扰 使用 LDO 稳压 + 适当的 旁路电容(0.1μF + 10μF)
浮空电容注入影响 增加前端 RC 滤波器 或缓冲运放
不希望采集浮值 固件中可判断采样值是否连续低于阈值,做“无信号”判断


这是调试的一个图,被干扰的样子

信号分析仪设计

我这里就用第一开始的稿子了:对于这个上位机,我首先想要丰富的数据分析呈现,所以时域,频域,时频分析是有的,而且 ADC 本身缺失了滤波器的功能,所以我加入了数字滤波器。

在开始采集后,会把所有的数据保存为 CSV,做后处理工作。

然后整体流程是这样的:

Start program
   │
   ▼
串口初始化,创建图形界面
   │
   ▼
定义 update() 函数:读取数据 → 更新图像
   │
   ▼
FuncAnimation() 注册 update 函数
   │
   ▼
plt.show() 启动事件循环(每隔 interval ms 调用一次 update)
   │
   ▼
 ┌────────────┐
 │每次刷新过程│
 └────────────┘
    └─> update()
         └─> 读取串口帧
         └─> 滤波处理
         └─> FFT 分析
         └─> 更新图形
         └─> 返回更新的 Line 对象



但是以下是最开始的程序设计,所以大体思路按照如下设计。

数据协议

MCU → 上位机 发送帧格式(总长 6 字节):

字节序号 内容 说明
0 0xAA 帧头(同步)
1 ADC 低字节 原始 ADC 值低 8 位
2 ADC 高字节 原始 ADC 值高 8 位
3 电压低字节 毫伏值低 8 位
4 电压高字节 毫伏值高 8 位
5 0x55 帧尾

安装依赖项(如果尚未安装):

pip install pyserial matplotlib scipy numpy

运行环境必须包含串口 COM16 ,并有设备按照帧结构:

帧头(0xAA) + 原始数据(2字节) + 电压mv(2字节) + 帧尾(0x55)

串口与数据采集配置

ser = serial.Serial("COM16", 115200, timeout=1)

打开串口 COM16,波特率 115200,超时时间 1 秒。

用于接收来自 STM32 的数据包(帧结构:0xAA + 原始数据(2B) + 电压mV(2B) + 0x55)

采样与滤波参数设置

BUFFER_SIZE = 512
SAMPLE_RATE = 1000  # Hz

使用一个环形缓冲区(deque )维护最近 512 个样本点。

采样率设为 1000Hz,决定 FFT 横坐标频率轴最大为 500Hz。

FILTER_LOW = 30
FILTER_HIGH = 200

默认带通滤波器的截止频率设置为 30Hz~200Hz。

CSV 数据保存初始化

csv_file = open(f"adc_log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", ...)
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["Sample", "Voltage (V)"])

创建一个时间戳命名的 CSV 文件;每帧数据记录一行(ADC 原始值,电压值)。

Matplotlib 界面搭建(两个子图)

fig, (ax_time, ax_freq) = plt.subplots(2, 1, figsize=(10, 6))

上图 ax_time :显示 时域波形(原始+滤波)

下图 ax_freq :显示 频域波形(FFT)

line_raw, = ax_time.plot(data, label="Raw")
line_filtered, = ax_time.plot(filtered_data, label="Filtered", linestyle='--')

蓝色实线:原始采样数据

虚线:带通滤波器输出

line_fft, = ax_freq.plot(np.linspace(0, SAMPLE_RATE/2, BUFFER_SIZE//2), np.zeros(BUFFER_SIZE//2))
peak_marker, = ax_freq.plot([], [], 'ro', label="Peak")

line_fft 是实时更新的频谱曲线

peak_marker 是频谱峰值的红色标记点

滤波器类型:二阶带通巴特沃斯滤波器

实时参数调节:

b, a = signal.butter(2, [low, high], btype='band', fs=SAMPLE_RATE)
filtered = signal.filtfilt(b, a, list(data))

低频和高频截止值由滑块 (Slider ) 控制

串口帧读取函数

def read_frame():
    while ser.read(1) != b'\xAA':
        pass
    frame = ser.read(5)
    if len(frame) < 5 or frame[-1] != 0x55:
        return None
    raw = frame[0] | (frame[1] << 8)
    voltage_mv = frame[2] | (frame[3] << 8)
    return raw, voltage_mv / 1000.0

保证同步从帧头 0xAA 开始读取

提取两部分内容:

raw :原始 ADC 码

voltage : 计算后电压值(单位 V)

主循环 update(frame) 动画函数

读取数据并更新缓冲区

parsed = read_frame()
raw, voltage = parsed
data.append(voltage)
csv_writer.writerow([raw, voltage])

动态生成带通滤波器(Butterworth)

b, a = signal.butter(2, [low, high], btype='band', fs=SAMPLE_RATE)
filtered = signal.filtfilt(b, a, list(data))

butter 生成二阶滤波器系数

filtfilt 是前向后向滤波,避免相位偏移

时域图更新

line_raw.set_ydata(data)
line_filtered.set_ydata(filtered_data)

频谱 FFT 更新

fft_data = np.fft.rfft(filtered - np.mean(filtered))
fft_freq = np.fft.rfftfreq(len(filtered), 1/SAMPLE_RATE)
fft_mag = np.abs(fft_data)
fft_mag /= np.max(fft_mag + 1e-8)

rfft:仅计算实信号的正频部分幅度归一化至 0~1

峰值频率注释

peak_idx = np.argmax(fft_mag)
peak_freq = fft_freq[peak_idx]
peak_amp = fft_mag[peak_idx]
peak_marker.set_data([peak_freq], [peak_amp])
ax_freq.set_title(f"FFT Spectrum - Peak: {peak_freq:.1f} Hz")

自动找出最大频率分量并在图中用红点标出;显示主频位置

按键事件绑定(ESC 退出)

def on_key(event):
    if event.key == 'escape':
        ser.close()
        csv_file.close()
        plt.close(fig)

实时动画渲染启动

ani = animation.FuncAnimation(fig, update, interval=10)
plt.show()

interval=10 毫秒刷新周期 ≈ 100Hz 界面更新速率

以上就是上位机的骨架,功能是在这个上面扩展的,可以给大家看一下现在的功能有哪些:






<完>