
简介这份资源面向嵌入式开发初学者与需要快速验证阻抗测量方案的工程师提供STM32F030RB通过I2C总线读写AD5933电导率/阻抗转换器的完整工程代码并借助UART将测量结果回传至PC端串口软件查看。包内共318个文件以h头文件、c源文件、o目标文件、crf与xcl编译中间文件为主另含hex烧录文件、map映射表、uvprojx与ewp等IDE工程文件及少量bat脚本压缩包约15.56MB可直接用Keil或IAR打开编译。代码覆盖I2C引脚与时钟配置、AD5933寄存器读写、频率与增益设置、UART数据上传以及中断与错误处理等环节适合对照数据手册逐层理解通信流程。目前已有2268人学习下载可作为物联网、环境监测与生物医学传感方向的入门参考。1. STM32 I2C 读写 AD5933为什么你的阻抗数据总在跳如果你手头有一块 AD5933 阻抗测量芯片用 STM32 的硬件 I2C 去读写它大概率会遇到一个很玄学的现象寄存器读回来是对的但阻抗实部虚部一直在跳换个频率跳得更厉害。这不是芯片坏了也不是 I2C 没通而是 AD5933 的读写时序和普通 I2C 从机完全不是一回事。它内部有状态机命令寄存器写下去之后需要等待扫描模式下每个频率点都要重新触发读数据必须按实部高字节、实部低字节、虚部高字节、虚部低字节的顺序连续读四个字节中间不能插别的操作。这篇笔记就围绕 STM32 硬件 I2C 读写 AD5933 这条链路把寄存器映射、单点测量、频率扫描、数据校准这几个环节拆开讲清楚适合已经能点灯、能跑 I2C 例程但卡在 AD5933 数据不稳定的嵌入式工程师。2. AD5933 的 I2C 从机地址与寄存器映射先把读写边界划清楚2.1 从机地址到底是 0x0D 还是 0x1AAD5933 的 7 位 I2C 地址固定为 0b0001101也就是 0x0D。但很多 STM32 的 HAL 库 I2C 函数要求传入的是左移一位后的 8 位地址所以你会看到有人写 0x1A有人写 0x0D然后互相觉得对方是错的。这里必须统一如果你用的是 HAL_I2C_Mem_Read / HAL_I2C_Mem_Write第二个参数 DevAddress 要传 0x0D 1 0x1A如果你用的是标准库或者自己写的底层时序传 0x0D 再在函数内部左移。我一般会在头文件里定义两个宏避免混用// AD5933 7位地址固定为 0x0D #define AD5933_ADDR_7BIT 0x0D // HAL库使用的8位地址 #define AD5933_ADDR_8BIT (AD5933_ADDR_7BIT 1) // 常用寄存器地址 #define AD5933_REG_CTRL_H 0x80 #define AD5933_REG_CTRL_L 0x81 #define AD5933_REG_START_FREQ 0x82 #define AD5933_REG_FREQ_INC 0x85 #define AD5933_REG_NUM_INC 0x88 #define AD5933_REG_SETTLE 0x8A #define AD5933_REG_STATUS 0x8F #define AD5933_REG_TEMP 0x92 #define AD5933_REG_REAL_H 0x94 #define AD5933_REG_REAL_L 0x95 #define AD5933_REG_IMAG_H 0x96 #define AD5933_REG_IMAG_L 0x97这段代码里最关键的是地址宏和寄存器偏移。AD5933 的寄存器地址是 8 位但每个参数占多个字节比如起始频率占 0x82、0x83、0x84 三个字节频率增量占 0x85、0x86、0x87增量数占 0x88、0x89。写的时候必须从高字节开始连续写读的时候也要按顺序读。HAL_I2C_Mem_Write 的 MemAddress 参数就是这里的寄存器地址MemAddSize 固定用 I2C_MEMADD_SIZE_8BIT。2.2 状态寄存器的两个关键位状态寄存器 0x8F 只有两个位有用bit1 是温度测量完成bit0 是频率点数据有效。单点测量时写启动命令后要轮询 bit0等它变 1 才能读实部虚部。扫描模式下每个频率点都要等一次 bit0。很多人读数据跳就是因为没等这个位直接延时几毫秒就去读结果读回来的是上一个点的残留值或者无效值。// 等待数据有效超时返回错误 uint8_t AD5933_WaitValid(uint32_t timeout_ms) { uint32_t tick HAL_GetTick(); uint8_t status 0; while (1) { HAL_I2C_Mem_Read(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_STATUS, I2C_MEMADD_SIZE_8BIT, status, 1, 100); if (status 0x02) { // bit1 温度有效 // 温度测量场景 } if (status 0x01) { // bit0 数据有效 return 0; } if (HAL_GetTick() - tick timeout_ms) { return 1; // 超时 } } }这个轮询函数里超时时间我一般给 100ms 以上因为 AD5933 内部 ADC 转换需要时间尤其是低频段。如果你把超时设成 10ms低频点很容易误判超时然后程序跑飞。注意读状态寄存器用单字节读不要用连续读因为状态寄存器后面跟着的是温度值连续读会多读一个字节虽然不影响状态位但容易让 I2C 时序变乱。2.3 控制寄存器的初始化顺序AD5933 上电后必须按固定顺序初始化先写控制寄存器高字节 0x80 设置量程和时钟源再写低字节 0x81 设置工作模式。控制寄存器高字节的 bit5 到 bit3 是输出激励范围bit2 到 bit0 是 PGA 增益。常见配置是输出 2Vpp、PGA 增益 1 倍对应 0x80 写 0x20 左右。低字节 0x81 的 bit4 到 bit0 是模式选择初始化时先写 0x00 进入待机再写 0x10 进入初始化最后写 0x20 进入单点测量或者 0x30 进入频率扫描。// AD5933 初始化序列 void AD5933_Init(void) { uint8_t ctrl_h 0x20; // 输出2VppPGA1 uint8_t ctrl_l 0x00; // 先进入待机 HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_H, I2C_MEMADD_SIZE_8BIT, ctrl_h, 1, 100); HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); HAL_Delay(10); // 进入初始化模式内部会校准 ctrl_l 0x10; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); HAL_Delay(50); // 回到待机等待后续命令 ctrl_l 0x00; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); }初始化里的延时不能省。待机到初始化之间至少 10ms初始化到待机至少 50ms这是 AD5933 内部校准需要的。如果你用 HAL_Delay 觉得浪费时间可以改成状态机轮询但新手建议先按这个延时来跑通再优化。控制寄存器高字节的 0x20 对应输出 2Vpp如果你测的是低阻抗负载可以改成 0x40 或者 0x60 提高激励但注意不要超过芯片的供电范围。3. 单点测量与频率扫描把实部虚部读回来的完整流程3.1 单点测量的四个步骤单点测量是最基础的用法适合先验证 I2C 链路和芯片是否正常工作。步骤是写起始频率、写控制寄存器进入初始化、写控制寄存器进入单点测量、轮询状态位、读实部虚部。这里最容易翻车的是起始频率的写入AD5933 的起始频率是 24 位分三个字节计算方式是频率值乘以 4 再除以时钟频率然后取整。假设你用 16MHz 内部时钟要测 10kHz那么起始频率码 10000 × 4 / 16000000 × 2^27算出来大约是 3355443转成十六进制是 0x333333分别写到 0x82、0x83、0x84。// 写入起始频率freq_hz 单位 Hz时钟 16MHz void AD5933_SetStartFreq(uint32_t freq_hz) { uint32_t code (uint32_t)((uint64_t)freq_hz * 4 * 134217728ULL / 16000000ULL); uint8_t buf[3]; buf[0] (code 16) 0xFF; buf[1] (code 8) 0xFF; buf[2] code 0xFF; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_START_FREQ, I2C_MEMADD_SIZE_8BIT, buf, 3, 100); } // 单点测量并读取实部虚部 void AD5933_SinglePoint(uint32_t freq_hz, int32_t *real, int32_t *imag) { uint8_t ctrl_l; uint8_t buf[4]; AD5933_SetStartFreq(freq_hz); // 进入初始化 ctrl_l 0x10; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); HAL_Delay(50); // 进入单点测量 ctrl_l 0x20; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); // 等待数据有效 if (AD5933_WaitValid(200) ! 0) { *real 0; *imag 0; return; } // 连续读四个字节实部高、实部低、虚部高、虚部低 HAL_I2C_Mem_Read(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_REAL_H, I2C_MEMADD_SIZE_8BIT, buf, 4, 100); *real (int16_t)((buf[0] 8) | buf[1]); *imag (int16_t)((buf[2] 8) | buf[3]); }这段代码里起始频率码的计算用了 64 位中间变量因为 32 位乘法在低频段可能溢出。实部虚部是 16 位有符号数读回来要强制转成 int16_t 再赋给 int32_t否则高位补零会变成大正数。连续读四个字节是关键不能分四次单字节读因为 AD5933 的数据寄存器在读完实部低字节后会自动指向虚部高字节分次读中间如果插了别的 I2C 操作指针会乱。3.2 频率扫描的增量与点数设置频率扫描需要设置三个参数起始频率、频率增量、增量数。频率增量的计算方式和起始频率一样也是 24 位写到 0x85、0x86、0x87。增量数写到 0x88、0x89是 9 位有效最大 511。扫描的总点数 增量数 1因为包含起始点。比如你要从 10kHz 扫到 100kHz分 10 个点那么增量 (100000 - 10000) / 10 9000Hz增量数 9。// 设置频率增量和增量数 void AD5933_SetSweep(uint32_t start_hz, uint32_t inc_hz, uint16_t num_inc) { uint8_t buf[3]; uint32_t code; // 起始频率 AD5933_SetStartFreq(start_hz); // 频率增量 code (uint32_t)((uint64_t)inc_hz * 4 * 134217728ULL / 16000000ULL); buf[0] (code 16) 0xFF; buf[1] (code 8) 0xFF; buf[2] code 0xFF; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_FREQ_INC, I2C_MEMADD_SIZE_8BIT, buf, 3, 100); // 增量数9位有效 buf[0] (num_inc 8) 0x01; buf[1] num_inc 0xFF; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_NUM_INC, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); }增量数的高字节只有 bit0 有效所以写的时候要 0x01否则可能把保留位写进去导致扫描点数不对。扫描模式启动后每个频率点都要等状态位 bit0然后读四个字节再写控制寄存器低字节的 0x30 触发下一个点。注意不是重新写 0x30而是写 0x30 之后芯片会自动递增频率你只需要等状态位再读数据。3.3 扫描模式下的数据读取循环扫描模式的数据读取循环是整段代码里最容易出问题的地方。正确的顺序是进入扫描模式、等待状态位、读四个字节、写 0x30 触发下一点、再等待。很多人写成读完之后重新写起始频率那样会回到第一个点永远扫不完。// 频率扫描结果存入数组 void AD5933_Sweep(uint32_t start_hz, uint32_t inc_hz, uint16_t num_inc, int32_t *real_buf, int32_t *imag_buf) { uint8_t ctrl_l; uint8_t buf[4]; uint16_t i; AD5933_SetSweep(start_hz, inc_hz, num_inc); // 进入初始化 ctrl_l 0x10; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); HAL_Delay(50); // 进入频率扫描 ctrl_l 0x30; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); for (i 0; i num_inc; i) { if (AD5933_WaitValid(200) ! 0) { real_buf[i] 0; imag_buf[i] 0; break; } HAL_I2C_Mem_Read(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_REAL_H, I2C_MEMADD_SIZE_8BIT, buf, 4, 100); real_buf[i] (int16_t)((buf[0] 8) | buf[1]); imag_buf[i] (int16_t)((buf[2] 8) | buf[3]); // 触发下一个频率点 ctrl_l 0x30; HAL_I2C_Mem_Write(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_CTRL_L, I2C_MEMADD_SIZE_8BIT, ctrl_l, 1, 100); } }循环里每次写完 0x30 之后不要立刻读状态要给芯片一点时间。AD5933_WaitValid 里的轮询本身就有延时效果但如果你把 I2C 速度设到 400kHz轮询间隔太短可能会读到旧的状态位。我一般会在轮询里加一个 1ms 的 HAL_Delay或者把 I2C 速度降到 100kHz。另外扫描结束后要把控制寄存器低字节写回 0x00 进入待机否则芯片一直处于测量状态功耗和发热都会上来。4. 避坑与排查AD5933 读写中最容易翻车的五个点4.1 读回来的实部虚部全是 0xFFFF 或 0x0000现象是状态位已经有效但读回来的四个字节全是 0xFF 或者全是 0x00。原因通常是 I2C 地址错了或者连续读的时候寄存器指针没对准。先确认 HAL_I2C_Mem_Read 的 DevAddress 是 0x1A 而不是 0x0D再确认 MemAddress 是 0x94 而不是 0x95。如果地址都对用逻辑分析仪抓一下波形看读操作后面有没有 ACK没有 ACK 说明从机没响应检查上拉电阻是不是 4.7k 以上AD5933 的 SDA/SCL 上拉要求比较严格太小了波形上升沿太陡会有振铃。4.2 扫描到一半数据突然不变了现象是前几个频率点数据正常后面几个点读回来和前面一模一样。原因是扫描模式下写 0x30 触发下一点的时机不对如果你在等状态位之前就写了 0x30芯片会忽略这次触发停在当前点。正确的顺序是等状态位有效、读数据、写 0x30、再等下一个状态位。另外增量数设置超过 511 会导致溢出扫描点数变成 0 或者乱跳检查 0x88 的高字节是不是只写了 bit0。4.3 低频段数据跳动特别大现象是 1kHz 以下的数据实部虚部跳动超过 10%高频段反而稳定。原因是 AD5933 内部 ADC 在低频段需要更长的建立时间而你的轮询超时太短或者激励信号还没稳定就读了。解决办法是把 WaitValid 的超时从 100ms 加到 500ms并且在进入单点测量或扫描之前先让芯片在待机模式下稳定 100ms 以上。如果还跳检查激励输出端的耦合电容AD5933 的 VOUT 引脚需要隔直电容容值不对会导致低频衰减。4.4 I2C 总线锁死HAL 库返回 HAL_BUSY现象是程序跑一段时间后 HAL_I2C_Mem_Read 一直返回 HAL_BUSY重启才好。原因是 AD5933 在内部转换期间会拉低 SCL 做时钟延展如果 STM32 的 I2C 没有配置时钟延展超时就会一直等。解决办法是在 I2C 初始化里把 ClockNoStretchMode 设为 I2C_NOSTRETCH_DISABLE允许时钟延展同时把 Timeout 参数从 100 改成 1000。如果已经锁死可以在错误处理里手动发送 9 个 SCL 脉冲解锁再重新初始化 I2C。4.5 温度读回来是 0 或者 0x7FFF现象是读温度寄存器 0x92 和 0x93结果不对。原因是温度测量需要单独触发写控制寄存器低字节 0x90 进入温度测量模式等状态位 bit1 有效再读两个字节。温度值是 14 位有符号数高字节的 bit7 到 bit0 和低字节的 bit7 到 bit6 组成低字节的 bit5 到 bit0 保留。计算公式是温度 代码 / 32单位摄氏度。如果你直接读 0x92 一个字节会丢掉低位精度差很多。5. 从原始数据到阻抗值校准与复数运算的落地技巧5.1 用已知电阻做增益系数校准AD5933 读回来的实部虚部是 ADC 原始码不是阻抗值。要得到阻抗必须先校准增益系数。拿一个已知阻值的电阻 R_cal接在 VOUT 和 VIN 之间测一个频率点的实部虚部计算增益系数 1 / (R_cal × sqrt(real² imag²))。之后测未知阻抗时阻抗模值 1 / (增益系数 × sqrt(real² imag²))。这个校准只在一个频率点有效如果要做宽频扫描每个频率点都要校准或者用分段校准。// 计算增益系数real/imag 是校准电阻的原始码 float AD5933_CalcGain(int32_t real, int32_t imag, float r_cal) { float mag sqrtf((float)real * real (float)imag * imag); if (mag 1.0f) return 0.0f; return 1.0f / (r_cal * mag); } // 用增益系数计算未知阻抗模值 float AD5933_CalcImpedance(int32_t real, int32_t imag, float gain) { float mag sqrtf((float)real * real (float)imag * imag); if (mag 1.0f) return 0.0f; return 1.0f / (gain * mag); }校准电阻的选择有讲究尽量选和你待测阻抗同一数量级的比如测 1kΩ 左右的负载就用 1kΩ 校准测 10kΩ 就用 10kΩ。校准电阻的精度至少 1%温漂越小越好。如果校准和测量之间隔了很久芯片温度变化会导致增益漂移建议每次上电都重新校准一次或者把校准系数存在 Flash 里但定期更新。5.2 相位角的计算与象限判断阻抗的相位角 atan2(imag, real)但 AD5933 的实部虚部符号和实际阻抗的象限有对应关系不能直接 atan2。常见做法是先判断实部虚部的符号再决定相位角要不要加 180 度。如果实部为负、虚部为正相位角在第二象限atan2 返回的是负值要加 180 度。如果实部为负、虚部为负相位角在第三象限atan2 返回的是正值要减 180 度。这个细节在测电容和电感时特别重要相位角错了等效模型就全错了。// 计算相位角单位度 float AD5933_CalcPhase(int32_t real, int32_t imag) { float phase atan2f((float)imag, (float)real) * 180.0f / 3.1415926f; if (real 0 imag 0) { phase 180.0f; } else if (real 0 imag 0) { phase - 180.0f; } return phase; }相位角的精度受实部虚部的信噪比影响如果某个频率点实部虚部都很小相位角会跳得很厉害。可以在软件里加一个阈值当 sqrt(real² imag²) 小于某个值时认为该点无效直接跳过或者用相邻点插值。这个阈值根据你的校准电阻和 PGA 增益来定一般取满量程的 1% 左右。5.3 多次采样取平均降噪AD5933 单次测量的噪声比较大尤其是高频段。我一般会在每个频率点连续测 4 到 8 次去掉最大值和最小值剩下的取平均。这样能把随机噪声压下去但会牺牲扫描速度。如果你测的是变化很慢的阻抗比如电池内阻或者土壤湿度多采几次完全值得。如果测的是动态过程那就只能单次靠硬件滤波和屏蔽来降噪。// 多次采样取平均丢弃最大最小值 void AD5933_AvgSample(uint32_t freq_hz, int32_t *real_out, int32_t *imag_out) { int32_t real_buf[8], imag_buf[8]; int32_t real_sum 0, imag_sum 0; int32_t real_max -32768, real_min 32767; int32_t imag_max -32768, imag_min 32767; uint8_t i; for (i 0; i 8; i) { AD5933_SinglePoint(freq_hz, real_buf[i], imag_buf[i]); if (real_buf[i] real_max) real_max real_buf[i]; if (real_buf[i] real_min) real_min real_buf[i]; if (imag_buf[i] imag_max) imag_max imag_buf[i]; if (imag_buf[i] imag_min) imag_min imag_buf[i]; } for (i 0; i 8; i) { if (real_buf[i] real_max || real_buf[i] real_min) continue; if (imag_buf[i] imag_max || imag_buf[i] imag_min) continue; real_sum real_buf[i]; imag_sum imag_buf[i]; } *real_out real_sum / 6; *imag_out imag_sum / 6; }这段代码里丢弃最大最小值之后剩下 6 个样本所以除以 6。如果你采样次数改成 4 次那就去掉最大最小各一个剩下 2 个除以 2。注意每次单点测量都要重新写起始频率和进入单点模式不能连续读因为 AD5933 的单点模式每次都要重新触发。这个函数跑起来比较慢8 次采样加轮询大概要 1 秒左右适合对速度要求不高的场景。5.4 用 DMA 减轻 CPU 轮询负担如果你觉得轮询状态位太占 CPU可以用 STM32 的 I2C DMA 模式把读四个字节的操作交给 DMACPU 只负责等 DMA 完成中断。但 AD5933 的状态位轮询没法用 DMA因为它是单字节读而且需要判断 bit0。折中方案是用定时器中断每隔 1ms 读一次状态寄存器状态有效再启动 DMA 读数据。这样 CPU 占用率能从 100% 降到 10% 以下适合多任务系统。// 定时器中断里轮询状态位有效则置标志 volatile uint8_t ad5933_data_ready 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { uint8_t status; if (htim-Instance TIM2) { HAL_I2C_Mem_Read(hi2c1, AD5933_ADDR_8BIT, AD5933_REG_STATUS, I2C_MEMADD_SIZE_8BIT, status, 1, 10); if (status 0x01) { ad5933_data_ready 1; } } }定时器中断里做 I2C 读要注意HAL_I2C_Mem_Read 是阻塞的超时给 10ms 足够。如果 I2C 总线忙这个读会失败但下一次中断还会再读不影响最终结果。主循环里检测到 ad5933_data_ready 置位后再启动 DMA 读四个字节读完清标志。这样比纯轮询优雅很多但代码复杂度上来了新手建议先把轮询版本跑稳再改 DMA。5.5 校准数据的存储与温度补偿如果你做的设备需要长期运行校准系数不能每次上电都重算要存在 Flash 或者 EEPROM 里。但存储的校准系数是在某个温度下测的温度变了系数会漂。AD5933 内部有温度传感器可以每隔一段时间读一次温度根据温度偏移量修正增益系数。修正公式是 gain_corrected gain_cal × (1 k × (T - T_cal))其中 k 是温度系数需要你自己测几组温度下的校准数据拟合出来。这个做法在精密测量里很常见但如果你只是做个大概的阻抗测量不做温度补偿也能用误差大概在 5% 以内。我自己的习惯是每次上电先做一次单点校准把增益系数存在 RAM 里然后每隔 10 分钟读一次温度如果温度变化超过 5 度就重新校准。这样既不用频繁写 Flash又能跟上温度漂移。校准电阻用 0.1% 精度的金属膜电阻焊在板子上靠近 AD5933 的位置减少走线引入的寄生参数。这套流程跑下来1kHz 到 100kHz 范围内的阻抗测量重复性能做到 1% 以内对于大多数电池内阻检测和生物阻抗应用足够了。希望帮到你。本文还有配套的精品资源点击获取