简介

目标电压(你设的) → STM32结合满量程(5V)和分辨率(65535)算出DAC码值 → DAC数模转换输出这个码值对应的实际电压 → ADC采样这个输出电压 → 在STM32里读回来得到电压(浮点数) → PID用目标电压减实际电压算误差 → PID算出新的输出值 (浮点数)→ 转成新码值给DAC → DAC输出新电压 → ADC继续采样 → 如果不稳定就继续算继续调。

DAC8586C部分

引脚识别

引脚 功能 说明(接STM32)
AGND 0V 模拟地
SCLK SPI时钟 SPI_SCK
MOSI SPI数据 SPI_MOSI
SYNC 片选(CS) SPI_CS/任意GPIO
LDAC 同步加载 GPIO(可接低电平)
CLR 清除输出 GPIO(高电平正常)
DAC0~DAC7 8个模拟电压输出通道

硬件基础

用到的GPIO功能:

推挽输出:可主动拉高或拉低,如SYNC-PB2(拉低发数据,拉高数据发完)

浮空输入:不控制引脚高低电平,只做读取,如DRDY-PD3(读取高电平数据正在转换中,低电平转换完成,数据已准备好)

复用功能:在功能不冲突时,可使用或在不使用时用作普通引脚输出控制电平

DAC8586C(把STM32输出的数字信号转为对应的电压)

LDAC:拉低时更新数据

DIN:数据输入(收到为高则收到1,反之则收到0)

软件SPI

为什么用软件SPI:因为选择让DAC和ADC用独立的引脚,独立分配引脚,而硬件SPI需要固定引脚、共用引脚(总线一致)并且mode模式需一致。

可通过CubeMX配置实现硬件SPI:想给DAC和ADC分配不同的固定引脚,可以用两个不同的SPI外设,如SPI1接DAC、SPI2接ADC,这样引脚独立,模式也可以独立,但每个外设的引脚还是固定的,且需要在CubeMX里分别配置。

我的软件 SPI 代码分析

c

delay_spi(1);

DAC8568_SCK_L;      // 下降沿(数据已读走)

delay_spi(1);

DAC8568_SCK_H;      // 上升沿

从我的时序看:

先设置 MOSI

然后产生下降沿(读取数据,这是第一个边沿,不符合mode3的CPHA=1)

然后产生上升沿(读取数据,这是第二个边沿,符合mode3定义)

实际上,DAC8568 数据手册说它支持 Mode 1 和 Mode 2,不是 Mode 3。但你的代码能工作,说明时序容差允许,也就是代码的时序和标准mode3不完全一致,但DAC能容忍。

码值计算

码值是 0 到 65535 的整数,计算公式是:码值 = 目标电压 / 满量程 × 65535。例如满量程 3.3V、目标 1.5V 时,码值 = 1.5 / 3.3 × 65535 ≈ 29788。DAC 芯片收到 29788 后,输出 1.5V。

写通道的数据流

写通道的函数调用链是:DAC8568_SetVoltage 计算码值,

然后调用 DAC8568_Write_DAC_Channel,写通道

DAC8568_Write_DAC_Channel 调用 ADC_CodeTo32BitBuffer 把命令、通道、数据打包成 32 位整数,然后拆成 4 个字节,

最后调用 DAC8568_Send4Bytes 发送4个字节。

发送过程是:拉低 SYNC(正在传输中),调用四次 DAC8568_SPI_WriteByte 发送 4 个字节,每次发送 8 个 bit,最后拉高 SYNC。

DAC8568_SPI_WriteByte 逐位发送:先根据当前位的值设置 MOSI 高或低,然后产生一个时钟脉冲(SCK 拉低再拉高),重复 8 次。

DAC 收到完整的 32 位命令后,解析出通道地址和数据

c

float mDatafloat;
uint16_t mDtashort;

更新对应通道的输出电压

顺序:计算码值 → 打包32位命令 → 拉低SYNC → SPI发送4字节 → 拉高SYNC → DAC输出

初始化

void DAC8568_Init(void)
{
    DAC8568_GPIO_Init();

    DAC8568_Write_Command(RESET);
    HAL_Delay(20);

    DAC8568_Write_Command(POWER_UP);
    HAL_Delay(10);

    DAC8568_Write_Command(SETUP_INTERNAL_REGISTER);//DAC芯片内部自带的一个高精度的电压参考源,用来决定输出电压的满量程范围
    HAL_Delay(10);
}

顺序:复位 → 上电 → 使能内部基准。基准使能是最关键的一步,缺少这步 REF 引脚为 0V,DAC 没有输出。

遇到的问题以及解决方法

 症状

1.DAC的REF引脚一直为0V(正常应为2.5V),DAC0输出始终为0V。SPI引脚用万用表测不到明显跳变。代码编译0错误0警告,逻辑看起来完全正确。

排查过程

第一阶段怀疑硬件:检查供电(5V和3.3V都正常),检查接线(CLR拉高、LDAC拉低、共地良好)。结论:硬件没问题。

第二阶段反复检查初始化代码,怎么看都觉得没问题。直到真相大白。

问题1:半字节交换(nibble swap)

DAC8568的32位命令格式要求 Control字段在低4位,而我在移植代码时写反了:

命令

错误写法

正确写法

复位

0x70

0x07

上电

0x40

0x04

内部基准使能

0x80

0x08

为什么0x80是错误的?

DAC8568的命令帧格式:

text

DB31-DB28 | DB27-DB24 | DB23-DB20 | DB19-DB4 | DB3-DB0

[Prefix]            [Control]         [Address]         [Data]         [Feature]

前缀                     控制                地址              数据          功能位

第一个字节buf[0](DB31-DB24)包含Prefix(高4位) + Control(低4位):

发出去的字节

二进制

Prefix

Control

DAC理解成什么?

0x80

1000 0000

8 (0x8)

0 (0x0)

错误命令,REF不被使能

0x08

0000 1000

0 (0x0)

8 (0x8)

内部基准使能 → REF=2.5V ✅

0x80看起来比0x08更"顺眼",但DAC不认!

问题二:测错引脚

修复命令后,REF终于变成2.5V了,但DAC0还是0V。折腾了半天,最后把所有输出引脚测了一遍,才发现我一直测的是DAC1,不是DAC0。板子上丝印太小,看岔了。程序设置的是OutA(对应DAC0),而我一直在测DAC1(通道B),当然一直是0V。

解决方法

修正命令字节:复位用0x07,上电用0x04,内部基准使能用0x08。确保宏定义完整(RESET、POWER_UP、SETUP_INTERNAL_REGISTER)。测对引脚:DAC0对应通道A,DAC1对应通道B,以此类推。调试时如果不确定,把8个通道全测一遍。

 经验教训

REF引脚是DAC8568的第一诊断点:2.5V代表基准使能成功,0V代表命令没发对。移植代码时不要假设原代码完全正确,每个命令都要对着数据手册核对。调试时先确认物理连接,引脚测对了再怀疑代码。如果某个引脚一直是0V,把周围几个引脚都测一遍,很可能测错了。越是简单的错误越难发现,因为太简单,根本不会往那想。

最终状态

REF=2.5V(内部基准使能成功),DAC0=1.5V(输出电压正确)。

ADS1256

引脚定义

和 DAC 不同,ADC 需要两根数据线:DIN 是 STM32 发命令给 ADC,DOUT 是 ADC 发数据给 STM32。DRDY 是 ADC 的输出引脚,告诉 STM32“数据准备好了”。

GPIO初始化

// 输出引脚:SCLK, DIN, CS, RST
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

// 输入引脚:DOUT
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

// 输入引脚:DRDY(带下拉电阻,确保不浮空)
GPIO_InitStruct.Pin = GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

复位时序:

ADS_RST_L;           // 拉低复位引脚
delay_us(1000);      // 至少等 0.5ms,这里给 1ms
ADS_RST_H;           // 拉高,释放复位
delay_us(100000);    // 等待 100ms 让芯片稳定

输出引脚是STM32主动控制电平的,包括SCLK(产生时钟)、DIN(发命令给ADC)、CS(选中设备)、RST(复位ADC)。输入引脚是STM32读取电平的,包括DOUT(接收ADC发来的数据)、DRDY(接收ADC发来的数据就绪信号)。DRDY固定是下降沿有效,从高变低时表示数据准备好了。复位时序是拉低RST至少0.5ms,然后拉高,再等待100ms让芯片稳定。

软件SPI时序

ADC 需要双向通信,所以有两个函数:发送和接收。

发送一个字节(STM32 发命令给 ADC)

c

static void ADS1256_Send8Bit(uint8_t data)
{
    for (int i = 0; i < 8; i++) {
        if (data & 0x80)
            ADS_DIN_H;      // 当前位是 1 → DIN 拉高
        else
            ADS_DIN_L;      // 当前位是 0 → DIN 拉低

        ADS_SCLK_H;         // 拉高时钟
        delay_us(1);
        data <<= 1;         // 左移,准备下一位
        ADS_SCLK_L;         // 拉低时钟,ADC 在下降沿锁存数据
        delay_us(1);
    }
}

接收一个字节(STM32 从 ADC 读数据)

c

static uint8_t ADS1256_Recive8Bit(void)
{
    uint8_t read = 0;
    for (int i = 0; i < 8; i++) {
        read <<= 1;         // 左移,腾出最低位
        ADS_SCLK_H;         // 拉高时钟
        delay_us(1);
        if (HAL_GPIO_ReadPin(ADS_DOUT_PORT, ADS_DOUT_PIN))
            read |= 0x01;   // DOUT 为高 → 这一位是 1
        ADS_SCLK_L;         // 拉低时钟
        delay_us(1);
    }
    return read;
}

SPI模式

 两个模块的 SPI 模式对比

| 模块 | SPI 模式 | 空闲电平 | 数据锁存边沿 | 实际代码 |

|------|----------|----------|-------------|----------|

| DAC8568 | Mode 3 | HIGH | 下降沿(CPHA=1EDGE) | SCK_L → SCK_H |

| ADS1256 | Mode 1 | LOW | 下降沿(CPHA=2EDGE) | SCK_H → SCK_L |

 当前接线方案

| 模块 | SCLK | MOSI | 说明 |

|------|------|------|------|

| DAC8568 | PA5 | PA7 | 软件 SPI,Mode 3 |

| ADS1256 | PD0 | PD1 | 软件 SPI,Mode 1 |

结论:两个模块使用不同的 SPI 引脚,互不干扰,不需要关心模式差异。

读写寄存器

写寄存器:

c

void ADS1256_WriteReg(uint8_t reg, uint8_t value)
{
    ADS_CS_L;
    ADS1256_Send8Bit(CMD_WREG | reg);   // 写寄存器命令 + 地址
    ADS1256_Send8Bit(0x00);              // 写 1 个寄存器
    ADS1256_Send8Bit(value);             // 要写的值
    ADS_CS_H;
}

读寄存器:

c

uint8_t ADS1256_ReadReg(uint8_t reg)
{
    uint8_t read;
    ADS_CS_L;
    ADS1256_Send8Bit(CMD_RREG | reg);   // 读寄存器命令 + 地址
    ADS1256_Send8Bit(0x00);              // 读 1 个寄存器
    delay_us(5);
    read = ADS1256_Recive8Bit();
    ADS_CS_H;
    return read;
}

配置 ADC

c

void ADS1256_CfgADC(uint8_t gain, uint8_t drate)
{
    ADS1256_WriteCmd(CMD_RESET);
    delay_us(10);

    uint8_t buf[4];
    buf[0] = (0 << 3) | (1 << 2) | (0 << 1);   // STATUS:自动校准使能
    buf[1] = 0x08;                               // MUX:AIN0 - AINCOM
    buf[2] = (0 << 5) | (0 << 3) | (gain << 0); // ADCON:增益
    buf[3] = drate;                              // DRATE:采样率

    ADS_CS_L;
    ADS1256_Send8Bit(CMD_WREG | 0);             // 从寄存器 0 开始
    ADS1256_Send8Bit(0x03);                     // 写 4 个寄存器
    ADS1256_Send8Bit(buf[0]);
    ADS1256_Send8Bit(buf[1]);
    ADS1256_Send8Bit(buf[2]);
    ADS1256_Send8Bit(buf[3]);
    ADS_CS_H;

    ADS1256_WriteCmd(CMD_SELFCAL);              // 自校准
    delay_us(50);
    while (!ADS_DRDY_IS_LOW());                 // 等待校准完成
}

读取 ADC 值

c

uint32_t ADS1256_GetAdc(uint8_t channel)
{
    // 1. 切换通道
    ADS1256_WriteReg(REG_MUX, (channel << 4) | 0x08);
    
    // 2. 启动转换
    ADS1256_WriteCmd(CMD_SYNC);
    delay_us(5);
    ADS1256_WriteCmd(CMD_WAKEUP);
    
    // 3. 等待 DRDY 变低(数据就绪)
    while (!ADS_DRDY_IS_LOW());
    
    // 4. 读取 24 位数据
    ADS_CS_L;
    ADS1256_Send8Bit(CMD_RDATA);
    delay_us(5);
    uint32_t read = (uint32_t)ADS1256_Recive8Bit() << 16;
    read |= (uint32_t)ADS1256_Recive8Bit() << 8;
    read |= ADS1256_Recive8Bit();
    ADS_CS_H;
    
    return read;
}

读芯片 ID

c

uint8_t ADS1256_GetChipID(void)
{
    uint8_t status = ADS1256_ReadReg(REG_STATUS);
    return (status >> 4);   // ID 在高 4 位,正常为 0x03
}

换算电压

ADS1256 是 24 位,读数范围 0 到 16777215。满量程 = 2 × VREF / PGA,VREF=2.5V,PGA=1 时满量程 = 5V。

c

float adc_to_voltage(uint32_t adc_raw)
{
    return (adc_raw / 16777215.0f) * 5.0f;
}

DRDY 引脚

DRDY 是 ADC 的输出引脚,固定以下降沿和低电平表示数据就绪,与 SPI 模式无关。转换期间 DRDY 为高电平,转换完成后从高变低(下降沿),并保持低电平直到数据被读走。

与 DAC 的对比

对比项 DAC8568 ADS1256
引脚 PA5、PA7、PB2 PD0-PD5
SPI 方向 只写 读写
片选 SYNC(PB2) CS(PD4)
数据线 MOSI DIN + DOUT
就绪信号 DRDY
分辨率 16 位 24 位

遇到的问题以及解决方法

问题1:芯片 ID 读出来是 0x0F(正常应为 0x03)

现象:

- ADS1256 ID = 0x0F

- ADC 读数一直是 16777215(满量程)

根因:

- 模块供电只有 3.45V,5V 器件需要 ≥4.75V

- SPI 通信时序可能也有问题

解决:

- 改接 ITECH 电源 5V 供电

- 确认 SPI 模式为 Mode 1(CPOL=0, CPHA=1)

---

 问题2:VCC 电压只有 3.45V(供电已改为 5V)

现象:

- 用万用表测 ADS1256 模块的 VCC 引脚,仍然只有 3.45V

根因:

- GND 经过 DAC 模块中转,地线上有压降

解决:

- 将 ADS1256 的 GND 直接并联到电源负极

- 不要经过其他模块中转

最终状态

串口监视器
ADS ID=0x03, ADC Ch0 = 2000154

ADS ID=0x03, ADC Ch0 = 2000312

ADS ID=0x03, ADC Ch0 = 2000401

ADS ID=0x03, ADC Ch0 = 2000214

ADS ID=0x03, ADC Ch0 = 2000782

ADS ID=0x03, ADC Ch0 = 2000509

| 模块 | 状态 |

|------|------|

| DAC8568 | ✅ 正常输出 |

| ADS1256 | ✅ ID=0x03,读数稳定 |

| 供电 | ✅ 5V + 3.3V 独立供电 |

| 接地 | ✅ 单点并联接地 |

---

经验总结

1. 5V 器件必须给足 5V,3.3V 供电是坑

2. GND 必须直接接到电源,不要经过其他模块中转

3. SPI 通信先确认模式(Mode 1 最常见)

4. 测不到信号时,把所有引脚测一遍(可能测错了)

5. 调试分步验证:先读 ID,再读数据

完整数据流

主循环读 ADC 得到实际电压,PID 计算目标电压和实际电压的误差,输出新的 DAC 码值,DAC 更新电压。ADC 再读,形成闭环。设目标 1.0V 时,ADC 读到 1.0V 左右,DAC 输出约 0.82V,系统稳定。

PID 控制代码分析(位置式PID:直接计算输出值)

初始化函数

c

void PID_Init(PID_t *pid, float kp, float ki, float kd, float setpoint)
{
    pid->kp = kp;
    pid->ki = ki;
    pid->kd = kd;
    pid->setpoint = setpoint;
    pid->integral = 0.0f;
    pid->prev_error = 0.0f;
    pid->out_min = 0.0f;
    pid->out_max = 5.0f;         // DAC 输出上限 5V
    pid->integral_limit = 20.0f; // 积分限幅
}

初始化时设置 P、I、D 系数、目标值、输出范围、积分限幅。输出上限 5V 对应 DAC 满量程。

参数变化自动复位

c

if (pid->kp != pid->kp_last || pid->ki != pid->ki_last ||
    pid->kd != pid->kd_last)
{
    PID_Reset(pid);  // 清积分和上次误差
    pid->kp_last = pid->kp;
    pid->ki_last = pid->ki;
    pid->kd_last = pid->kd;
}

在线修改 PID 参数时,积分累计值可能和新参数不匹配,导致输出突变。这个设计在参数变化时自动清积分,防止突变。

积分项(带 dt 和限幅)

c

pid->integral += error * dt;  // 误差 × 时间
if (pid->integral > pid->integral_limit)
    pid->integral = pid->integral_limit;
i_term = pid->ki * pid->integral;

积分项是误差随时间累加。乘以 dt 让积分与采样时间无关,不同采样率下积分速度一致。积分限幅防止积分过大,目标变化时不会严重过冲。微分项

c

d_term = pid->kd * (error - pid->prev_error) / dt;
pid->prev_error = error;

微分项是误差变化率除以 dt,也与采样时间无关。误差变化越快,微分项越大,起抑制震荡的作用。

前馈

c

pid->output = p_term + i_term + d_term + pid->ff;

输出 = P + I + D + 前馈。前馈单独设置,不经过 PID 计算,直接加到输出上。你在主循环中根据开环标定曲线计算前馈值,赋给 pid->ff。

输出限幅

c

if (pid->output > pid->out_max) pid->output = pid->out_max;
if (pid->output < pid->out_min) pid->output = pid->out_min;

输出限制在 out_min 和 out_max 之间,防止 DAC 输出超出硬件范围(0V 到 5V)。

与你的前馈配合

主循环中:

c

// 前馈:根据目标电压计算 DAC 基础值
float ff = (target - 0.197f) / 0.972f;
pid.ff = ff;

// PID 计算微调量
float output = PID_Update(&pid, actual, dt);

// output 已经是前馈 + P + I + D,直接发给 DAC
DAC8568_SetVoltage(OutA, output);

关键设计

设计 作用
参数变化复位积分 在线调参时防止输出突变
积分限幅 防止积分饱和导致过冲
dt 归一化 采样时间变化不影响 P、I、D 增益
前馈偏置 补偿 DAC 和 ADC 之间的固定偏差

调试参数

从 P=0.5、I=0.2、D=0 开始,发现收敛慢。调大 P 到 2.0 后响应变快。调大 I 后稳态误差更小。D 在实际系统中容易放大噪声,所以保持为 0。前馈是最关键的优化,让 DAC 一步到位,PID 只需要微调。

问题以及解决方法

问题: PID从0V慢慢爬目标值(如设2.0V需要几十秒),因为PID直接

  输出DAC电压,但DAC和ADC之间有偏置(DAC=0.82V才能得到ADC=1.0V

  ),积分要从0慢慢累积。

  解决:

  用开环标定曲线(ADC=0.972×DAC+0.197)算出模型前馈值:dac_base

   = (目标 - 0.197) / 0.972,PID只输出±2.0V的微调量。设目标后DA

  C一步到位,PI只做细微修正。

  结果: 设目标2.0V → ADC瞬间跳到1.97V ✅

  波形模式命令补全(CMD_WAVE_AMP = 0x24)

  问题: wave_amp(幅度)字段存在,但没有协议命令能设置它,导致

  波形幅度一直为0,输出平在偏置电压上。

  解决: 在 protocol.h/protocol.c/test_host.py 中新增

  CMD_WAVE_AMP = 0x24 命令和交互命令 a。

  采样率提升(30SPS → 100SPS)

  问题: ADC采样率30SPS,1Hz波形每周期只有约30个采样点,绘出来

  的曲线不平滑。

  解决: DATARATE_30 → DATARATE_100,每个周期采样点从30提升到10

  0,波形平滑度明显改善。

  实时绘图模式

  结果: 新增 -m plot 模式,用matplotlib同时显示ADC(蓝)和DAC

  (红)波形曲线,交互模式下按 p 启动。

上位机协议

协议的作用

串口传输的是字节流,没有头尾区分。协议的作用是定义帧格式,让接收方能识别一条完整消息。

帧格式

接收帧(PC → STM32)36 字节:

字节位置 内容 说明
0 0x68 帧头
1-4 保留 填充 0x00
5 命令码 表示要执行什么操作
6-9 保留 填充 0x00
10-13 data1 4 字节数据(大端序)
14-17 data2 4 字节数据(大端序)
18-21 data3 4 字节数据(大端序)
22-33 保留 填充 0x00
34 校验和 0xFF - 累加和
35 0x16 帧尾

注意:你的代码中命令码在 rx_buf[5],第一个数据在 rx_buf[10],不是 buf[1] 和 buf[2]

命令码

c

#define CMD_DEVICE_CHECK   0x01   // 查询设备参数(回复86字节)
#define CMD_LD_ENABLE      0x02   // 开启激光
#define CMD_LD_DISABLE     0x03   // 关闭激光
#define CMD_LOOP_CHECK     0x18   // 查询实时数据(回复82字节)
#define CMD_CURRENT_SET    0x11   // 设置目标电流
#define CMD_BIAS_V1_SET    0x13   // 设置目标电压(mV)
#define CMD_PID_SET        0x1C   // 设置 PID 参数
#define CMD_FF_SET         0x1D   // 设置前馈值
#define CMD_WAVE_MODE      0x21   // 设置波形模式(0=关,1=正弦,2=方波,3=三角波)
#define CMD_WAVE_FREQ      0x22   // 设置波形频率(mHz)
#define CMD_WAVE_OFFSET    0x23   // 设置波形偏置(mV)
#define CMD_WAVE_AMP       0x24   // 设置波形幅度(mV)
#define CMD_DAC_DIRECT     0x20   // 开环直接设 DAC 电压(mV)

数据解析

c

void Protocol_Parse(uint8_t *buf, uint8_t len)
{
    // 检查帧头和帧尾
    if (buf[0] != 0x68 || buf[35] != 0x16) return;
    
    // 检查校验和
    uint8_t sum = 0;
    for (int i = 1; i < 34; i++) sum += buf[i];
    if (buf[34] != (0xFF - sum)) return;
    
    // 提取命令和数据(注意偏移量)
    uint8_t cmd = buf[5];
    uint32_t data1 = BIG_TO_U32(&buf[10]);
    uint32_t data2 = BIG_TO_U32(&buf[14]);
    uint32_t data3 = BIG_TO_U32(&buf[18]);
    
    // 命令分发
    switch (cmd) {
        case CMD_BIAS_V1_SET:  // 0x13 设置目标电压
            sys_params.setpoint = data1;  // 单位 mV
            Send_Standard_Reply(cmd, 0x00);
            break;
            
        case CMD_PID_SET:  // 0x1C 设置 PID 参数
            sys_params.pid_p = data1;
            sys_params.pid_i = data2;
            sys_params.pid_d = data3;
            Send_Standard_Reply(cmd, 0x00);
            break;
            
        case CMD_LOOP_CHECK:  // 0x18 查询实时数据
            Send_Collect_Reply();
            break;
            
        case CMD_DEVICE_CHECK:  // 0x01 查询设备参数
            Send_Settings_Reply();
            break;
            
        // ... 其他命令
    }
}

数据打包发送

c

void Send_Collect_Reply(void)
{
    uint8_t tx_buf[82];
    memset(tx_buf, 0, 82);
    
    tx_buf[0] = 0x68;
    tx_buf[5] = CMD_LOOP_CHECK;
    tx_buf[7] = 0x48;  // 数据长度 72 字节
    
    // 填入 ADC 测量值(mV)
    U32_TO_BIG(sys_params.ld_volt_meas, &tx_buf[20]);
    // 填入 DAC 输出值(mV)
    U32_TO_BIG(sys_params.ld_current_meas, &tx_buf[24]);
    // 填入温度
    U32_TO_BIG(sys_params.temp_meas, &tx_buf[48]);
    
    // 校验和
    uint8_t sum = 0;
    for (int i = 1; i < 80; i++) sum += tx_buf[i];
    tx_buf[80] = 0xFF - sum;
    tx_buf[81] = 0x16;
    
    UART_DMA_Send(tx_buf, 82);
}

DMA + IDLE 接收

c

void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 停止 DMA,计算接收长度
        HAL_DMA_Abort(huart->hdmarx);
        uint32_t len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
        
        if (len == 36) {
            Protocol_ProcessPacket(rx_buffer);
        }
        
        // 重新启动 DMA 接收
        HAL_UART_Receive_DMA(huart, rx_buffer, RX_BUF_SIZE);
    }
}

Python 上位机

python

import struct

def make_packet(cmd, data1=0, data2=0, data3=0):
    buf = bytearray(36)
    buf[0] = 0x68           # 帧头
    buf[5] = cmd            # 命令码(注意位置是5)
    
    # 数据是大端序
    struct.pack_into('>I', buf, 10, data1)   # data1(大端)
    struct.pack_into('>I', buf, 14, data2)   # data2
    struct.pack_into('>I', buf, 18, data3)   # data3
    
    # 校验和(字节 1-33,跳过帧头)
    sum_bytes = 0
    for i in range(1, 34):
        sum_bytes += buf[i]
    buf[34] = (0xFF - sum_bytes) & 0xFF
    
    buf[35] = 0x16          # 帧尾
    return buf

上位机测试脚本(test_host.py)

  任务

  创建/完善PC端测试工具

  功能

  - 6种模式:interactive / quick / monitor / calibrate / wave /

   plot

  - 交互命令:1~9、0、w、f、o、a、p、q

  - 实时绘图:matplotlib显示ADC(蓝)+DAC(红)波形曲线

  - 采集数据显示:ADC/DAC/Δ(偏差)/温度

  ---

  遇到的问题及解决

  问题: 固件没烧录成功

  现象: 设目标2.0V但ADC卡在1.0V,模型前馈没生效

  原因: Keil工程有多个Target输出路径,build/目录的.hex是旧版,

  而且点Build不一定全量重编译

  解决: 做 Project → Rebuild All,确认.hex时间戳更新后再烧录

  ────────────────────────────────────────

  问题: 幅度为0

  现象: 波形模式DAC输出平在偏置电压,没有波动

  原因: wave_amp 没有对应的协议命令,上位机设了也白设

  解决: 新增 CMD_WAVE_AMP = 0x24 命令

  ────────────────────────────────────────

  问题: 三种波形看不出区别

  现象: 正弦/方波/三角波在图上差不多

  原因: 采样率太低(30SPS) + 频率太高(1Hz)

  解决: 降频率到0.2Hz + 提速到100SPS,数值变化更能看出区别

  ────────────────────────────────────────

  问题: matplotlib报错

  现象: ModuleNotFoundError: No module named 'matplotlib'

  原因: 没装绘图库

  解决: pip install matplotlib

  ────────────────────────────────────────

  问题: 回复显示错乱

  现象: 设目标电压后显示CMD=0x20而不是0x13

  原因: 串口回复帧和查询帧在后台线程中交错

  解决: 不影响功能,实际命令已正确执行

问题:积分限幅太小,积分器"有力使不出"。

  - integral_limit=2.0:积分最大贡献 = 0.2 × 2.0 =

  0.4V,高目标推不上去

  - integral_limit=20.0:积分最大贡献 = 0.2 × 20.0 =

  4.0V,全范围都能锁

  2.0就像给积分器绑了手脚,20.0解开了。实际积分不会真跑到20,只

  是个保险上限。

Github仓库地址:github.com/WuQinghui-00/STM32F407-DAC8568-ADS1256-PID

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐