电压闭环控制系统
简介
目标电压(你设的) → 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
更多推荐
所有评论(0)