AD7606+STM32F407以太网TCP高速上传:FSMC与LwIP关键配置
2026/9/13 14:32:30 网站建设 项目流程

简介:基于 STM32F407 与 AD7606 的 16 位 8 通道数据采集及 TCP/IP 网络传输完整工程源码,面向嵌入式开发者、毕设/电赛备赛者及工业远程监测项目起步。程序覆盖 AD7606 的 SPI 高速采样、数值滤波处理,再到 STM32F407 以太网 MAC 外设的 LwIP 协议栈接入,包含 TCP 客户端连接、数据打包上送与连接管理流程,可直接用作远程监控、工业自动化等场景的数据采集节点参考。资源包共 561 个文件,以 h/c 源码为主体(151 个 h、134 个 c),另有 d/o/crf 等编译中间文件、uvprojx/uvoptx 工程配置及 hex 烧录文件,压缩包约 11.46MB,便于对照工程结构二次开发。已有 471 人学习下载,适合需要完整源码与移植参考的开发者,可快速理清 AD7606 驱动、LwIP 集成及 TCP 数据上云的设计思路。

1. 把 AD7606 采集数据用 STM32F407 走 TCP/IP 上传,难点不在网口

把 AD7606 采集的数据经 STM32F407 以太网口以 TCP/IP 方式上传,是振动监测、电能质量分析一类项目里最常见的需求组合。AD7606 每次转换产生 8 通道 × 16 位共 16 字节,采样率一上 50kSPS,串口就顶不住了;F407 内置以太网 MAC,外接一颗 DP83848 就能提供百兆通道,硬件条件并不苛刻。

方案不新,常见翻车点也不在 PHY 能不能协商成功,而在 AD7606 读取时序和 LwIP 发送节流之间的衔接:采样中断里多耗一个微秒,TCP 写缓冲一满,丢帧就从这里开始。所以这个网络数据传输程序的功底体现在三处——AD7606 用哪种接口读进内存、缓冲谁在填谁在发、以及采样率与发送批量怎么匹配。

下面按采样端、网络端、数据链路和排错四层推进,每个环节给出能直接抄的参数、代码和判断依据。着急先跑通的,直接从第 2 章的 FSMC 并口配置开始移植即可。

2. 读 AD7606 的两种方式:FSMC 并口和 SPI 该怎么选

AD7606 的转换流程是:CONVST 引脚上跳沿启动一次转换,8 个通道同步采样,BUSY 变为高电平,转换结束后 BUSY 下降沿拉低,此时结果锁存在内部寄存器里等待读取。读取接口分并口和串行两类,对 STM32F407 而言,真正要做决策的是 16 位并口(走 FSMC)和 SPI 二选一,前者决定采样率上限,后者决定引脚代价。数据手册标称 AD7606 吞吐为 200kSPS,但实际工程里能跑多快,取决于读出 8 个通道结果花了多久。

2.1 AD7606 并口、SPI、菊花链三种接口怎么选

三种接口的本质差异是读走 8 通道结果需要多少个时钟周期。并口模式下,一个 CS+RD 读周期取一个通道,8 次读取在亚微秒内完成;SPI 模式至少要把 128 位移出芯片,SCK 跑 16MHz 也要 8us,再算上转换和协议开销,实际吞吐会明显低于标称值。低速采集用 SPI 省引脚,超过 100kSPS 建议直接并口。

接口方式引脚开销读 8 通道耗时工程可用采样率适用场景
16 位并口(FSMC)约 20 根亚微秒100kSPS 以上振动、录波
SPI 单线4~6 根128bit ÷ SCK,16MHz 时约 8us50kSPS 以下隔离、紧凑布线
SPI 菊花链4~6 根随级联芯片数成倍增加更低多片 AD7606 级联

菊花链适合把多片 AD7606 串在一条 SDO 线上省引脚,代价是读出周期随片数线性增长,一般不用于单芯片项目。还有一个容易忽略的点:SPI 模式要多留出 CPU 在读取期间被中断抢占的时间,所以“理论 SCK 算出来够用”和“实测够用”之间往往差一个量级。

2.2 并口接法:把 AD7606 映射成 FSMC 地址空间

FSMC 的工程价值是用硬件时序代替 GPIO 翻转。把 AD7606 的 D0~D15 接 FSMC 数据线,RD 接 NOE,CS 接到 NE1 或一根在基地址处为低电平的地址线,RESET、RANGE、OS0~OS2、CONVST、BUSY 接普通 GPIO。之后对 0x60000000 基地址的每一次读操作,硬件自动完成“片选选通 + RD 脉冲”,等价于一次并口读周期。VET6 的 100 脚封装上 FSMC Bank1 依然可用,但地址线不是全部引出,具体接哪一根要对着该封装的引脚表确认,别直接抄 144 脚图纸。

FSMC 时序初始化里真正要调的是 DataSetupTime,它决定 RD 低电平宽度。AD7606 手册要求 RD 最小脉宽约 20ns,168MHz 主频下一个 HCLK 是 5.95ns,所以取 4 个周期约 23.8ns 作为安全下限:

static void AD7606_FSMC_Init(void) { SRAM_HandleTypeDef hadc_sram; FSMC_NORSRAM_TimingTypeDef timing = {0}; FSMC_NORSRAM_InitTypeDef init = {0}; hadc_sram.Instance = FSMC_NORSRAM_DEVICE; hadc_sram.Extended = FSMC_NORSRAM_EXTENDED_DEVICE; __HAL_RCC_FSMC_CLK_ENABLE(); /* D[0:15]、NOE、NE1、A6 均配置为 AF12 复用模式,GPIO 配置略 */ timing.AddressSetupTime = 1; /* 1 × 5.95ns,地址建立时间 */ timing.AddressHoldTime = 0; timing.DataSetupTime = 4; /* RD 低电平约 23.8ns,对应 tRD */ timing.BusTurnAroundDuration = 0; timing.CLKDivision = 16; timing.DataLatency = 0; timing.AccessMode = FSMC_ACCESS_MODE_B; init.FSMC_DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; init.FSMC_MemoryType = FSMC_MEMORY_TYPE_SRAM; init.FSMC_MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; init.FSMC_BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; init.FSMC_WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; init.FSMC_WrapMode = FSMC_WRAP_MODE_DISABLE; init.FSMC_WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_RS; init.FSMC_WriteOperation = FSMC_WRITE_OPERATION_DISABLE; init.FSMC_WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; init.FSMC_ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; init.FSMC_WriteBurst = FSMC_WRITE_BURST_DISABLE; init.FSMC_ReadWriteTimingStruct = &timing; init.FSMC_WriteTimingStruct = &timing; HAL_SRAM_Init(&hadc_sram, &init, &timing); }

这里有两个值得记住的坑。第一,GPIO 必须全部改成复用功能 AF12,漏掉一根数据线时现象很典型:某一通道读回来全是 0xFF,而其它通道正常。第二,写操作要禁止,AD7606 没有寄存器可写,FSMC_WriteOperation 设为 DISABLE,防止意外的总线写把数据线驱动成输出。

转换触发建议用定时器 PWM 直接驱动 CONVST,采样率完全由硬件决定,不受主循环抖动影响。读取时注意 AD7606 没有地址总线,并口模式下每来一次 CS+RD 读脉冲,内部通道指针自动递增一次,所以对同一个基地址连续读 8 次即可得到通道 1~8 的完整一帧:

static volatile uint8_t adc_done; /* BUSY 中断置位 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == BUSY_Pin) { adc_done = 1; /* 只置标志,不做读取 */ } } void AD7606_ReadAll(uint16_t buf[8]) { volatile uint16_t *dev = (volatile uint16_t *)0x60000000u; for (int i = 0; i < 8; i++) { buf[i] = dev[0]; /* 同地址连读 8 次,通道指针自动递增 */ } }

这段代码里最容易写错的是地址。AD7606 既然没有地址线,就不存在“每通道一个地址”的说法;把 CS 接在 A6 上时,基地址 0x60000000 的 A6 位为 0,CS 有效,读同一个地址即可。如果换成 CS 接 NE1 也完全成立,地址偏移那一列直接从程序里删掉。中断里只置位标志、不执行 FSMC 读取,是避免 BUSY 中断把时间片耗尽的基本操作;等主循环或采集任务空闲时再批量读走。

提示:如果采样率低于 10kSPS,完整读取耗时可忽略;高于 100kSPS 时建议把读取从中断挪到高优先级任务里,并在两次 BUSY 下降沿之间预留足够裕量。OS0~OS2 全部接低电平关掉过采样,高速采集时过采样倍数会直接压掉吞吐上限。

2.3 SPI 方式:VET6 引脚紧张时的省脚替代

STM32F407VET6 是 100 脚封装,FSMC 数据总线又占了不少引脚,项目同挂显示屏、SD 卡后,并口方案显得很挤。此时 AD7606 改用 SPI 是常规退路。接法只有五根线:SCLK、CS、DOUT、BUSY、CONVST,8 个通道的结果按顺序出现在 SDO 上。

static uint8_t spi_rx[16]; void AD7606_ReadViaSPI(uint16_t buf[8]) { while (adc_done == 0); /* BUSY 下降沿之后才能读 */ HAL_SPI_Receive(&hspi1, spi_rx, 16, 100); for (int i = 0; i < 8; i++) { buf[i] = (uint16_t)((spi_rx[i << 1] << 8) | spi_rx[(i << 1) | 1]); } adc_done = 0; }

SCK 从 8MHz 起步,逐级往上加。超过 16MHz 后,AD7606 输出数据的保持时间余量变小,长走线时容易在最高位附近抽样出错,波形上表现为固定的毛刺包络。若用 DMA 接收这 16 字节,缓冲区必须放在 0x20000000 起始的主 SRAM 段,不能放到 F407 的 CCM RAM(0x10000000):DMA 控制器访问不到 CCM 区域,这是 stm32f407 板子上 DMA 搬运“读数全零”的最常见原因。

SPI 方案还需要接受一个代价:128 位读出过程中,CPU 或 DMA 占用是连续的,如果此时网卡中断涌进来,读取会被延迟,BUSY 已经拉高新一轮转换启动,前一轮数据就被覆盖。低速应用(50kSPS 以下)无所谓,高速应用仍应回到并口。

3. DP83848 + LwIP 把 STM32F407 变成 TCP 服务器

F407 的以太网外设由 MAC 和专用 DMA 组成,不包含 PHY。DP83848 是这个平台最常见的 PHY 之一,支持 MII/RMII;RMII 把 F407 到 PHY 的信号压到 7 根,适合引脚紧张的板子。下面从接线、协议栈配置到服务器代码给出可用路径。

3.1 RMII 接线与 50MHz 参考时钟从哪来

RMII 模式两组数据位、独立的 TX_EN 和 CRS_DV,参考接法如下。注意 REF_CLK 是 F407 的输入,时钟由 PHY 侧产生。

STM32F407 引脚RMII 信号方向
PA1REF_CLKPHY → MCU,50MHz
PA7CRS_DVPHY → MCU
PC4 / PC5RXD0 / RXD1PHY → MCU
PB11TX_ENMCU → PHY
PB12 / PB13TXD0 / TXD1MCU → PHY
PA2 / PC1MDIO / MDC配置管理接口

50MHz 参考时钟有三种常见来源:DP83848 的 XI 接 25MHz 晶振、由 CLK_OUT 引脚输出 50MHz;直接用 50MHz 有源晶振接 PA1;少数板子让 MCU 的 MCO 引脚输出 50MHz。时钟来源不同,PHY 的 strap 引脚电平配置就不同,上电前先看手册的 RMII 配置段,别照抄别家原理图。

PHY 复位是另一个高频翻车点。复位引脚要拉低至少 10ms,再等内部上电跑稳,很多程序只给 1ms 就开始读 PHY ID,MDIO 读回 0xFFFF,自动协商永远不成功。DP83848 默认 PHY 地址常见是 0x01,LAN8720 是 0x00;LwIP 底层初始化里 PHY 地址写错时表现一模一样:link 状态为 up,但收不到任何数据。

3.2 lwipopts.h 里决定 TCP 吞吐的宏

LwIP 裸机能跑,但采集任务和协议栈任务挤在同一线程时,TCP 重传、ARP 处理会抢占采样中断的响应。工程上常规做法是让 LwIP 跑在 FreeRTOS 的 tcpip_thread 上,采集任务和发送任务独立,LwIP 移植包的线程配置大多来自开发板例程,改动点集中在 lwipopts.h。

#define NO_SYS 0 /* 运行在 FreeRTOS 上 */ #define MEM_SIZE (64 * 1024) /* 协议栈堆内存 */ #define MEMP_NUM_PBUF 32 #define PBUF_POOL_SIZE 20 #define TCP_MSS 1460 /* 与 MTU 1500 匹配 */ #define TCP_WND (8 * TCP_MSS) /* 收端窗口,至少 8 个段 */ #define TCP_SND_BUF (8 * TCP_MSS) /* 发送缓冲 */ #define LWIP_NETCONN 1 /* 启用 netconn API */ #define LWIP_SOCKET 0 /* 不用 socket,省内存 */ #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_TCP 1 #define LWIP_DHCP 1 /* 联调期用 DHCP */

TCP_MSS 和 MTU 必须配套。缺省 MSS 只有 536,若上下层没统一改,IP 层会把大包分片,百兆链路的吞吐直接减半。TCP_WND 与 TCP_SND_BUF 越大,吞吐上限越高,但每加一档都占用一段连续 SRAM;F407 的 192KB 内存还要留给 AD7606 乒乓缓冲和网卡 DMA 描述符,实用上 8 到 16 个 MSS 是平衡点。

3.3 netconn API 写 TCP 服务器与断线清理

采集设备更适合当 TCP 服务器:上位机连接上来就持续发数据,断开就回到 accept 等待,不需要知道上位机 IP,也没有客户端反复重连造成的半开连接堆积问题。

static void tcp_server_task(void *arg) { struct netconn *srv = netconn_new(NETCONN_TCP); netconn_bind(srv, IP_ADDR_ANY, 5001); netconn_listen(srv, 1); for (;;) { struct netconn *cli = NULL; if (netconn_accept(srv, &cli) == ERR_OK) { xTaskCreate(tcp_stream_task, "stream", 384, cli, 5, NULL); } } } static void tcp_stream_task(void *arg) { struct netconn *cli = (struct netconn *)arg; tcp_nagle_disable(cli->pcb.tcp); /* 关 Nagle,降小帧时延 */ while (1) { err_t err = netconn_write(cli, tx_frame, frame_len, NETCONN_COPY); if (err != ERR_OK) { /* 对端关闭或链路断开 */ netconn_close(cli); netconn_delete(cli); vTaskDelete(NULL); } vTaskDelay(pdMS_TO_TICKS(5)); } }

关闭 Nagle 后,小帧会立即发出,发送时延分布更均匀,但代价是每个 TCP 段只装几十字节数据时头部开销飙升。正确做法是“关 Nagle + 攒段”:采样端把多次 AD7606 转换结果拼成一个接近 MSS 的帧再 netconn_write,时序低延迟和带宽利用两不误。netconn_write 使用 NETCONN_COPY 时是同步阻塞的,发送缓冲区满会一直等到协议栈腾出空间,这个背压特性恰好可以用来给采集端控速。

4. 样本帧、乒乓缓冲与采样率预算:AD7606 数据怎么过 TCP 不丢

4.1 上位机要能认帧:自定义帧头 + 帧序号

TCP 是字节流,接收端必须自己分帧。常见的做法是每包发一个固定头部,再跟一批样本。头部字段要能回答三个问题:这包数据从哪开始、是第几包、采样时刻是什么。

偏移字段长度说明
0magic20xAA5A,用于同步
2帧序号2每帧加 1,校验丢包
4通道数1固定 8
5采样率 kSPS1如 50
6时间戳4DWT 计数换算的时间

样本区紧跟其后,每 16 字节一组(8 通道 × 16 位)。一帧装 80 次采样时数据区 1280 字节,加头部后与 TCP_MSS 接近,发送效率最好。

typedef struct __attribute__((packed)) { uint16_t magic; uint16_t seq; uint8_t ch_num; uint8_t rate; uint32_t ts; } frame_hdr_t; /* 共 10 字节 */ /* 使用 DWT 前先使能 TRCENA 并清零 CYCCNT */ CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; frame_hdr_t hdr = { .magic = 0xAA5A, .seq = frame_seq++, .ch_num = 8, .rate = 50, .ts = (uint32_t)(DWT->CYCCNT / 168000000u) };

时间戳用 DWT 的 CYCCNT 比 SysTick 更省心:SysTick 的节拍会被 RTOS 调整,而 CYCCNT 是纯硬件计数器,精度到微秒级。转换成电压时开启 F407 的硬件浮点,一条raw * 10.0f / 32768.0f就能批量做标度变换,编译器加-mfpu=fpv4-sp-d16 -mfloat-abi=hard后不占用多少 CPU。

4.2 乒乓缓冲:采集中断填一边,发送任务读另一边

样本是连续产生的,而 netconn_write 是阻塞发送,二者直接同用一块缓冲会互相覆盖。常规解法是双缓冲:中断往当前缓冲区填充,填满一帧后切到另一块,发送任务只发已经填满的那块。

#define FRAME_SIZE 80u static uint16_t adc_buf[2][FRAME_SIZE][8] __attribute__((aligned(32))); static volatile uint8_t wr_idx; static volatile uint16_t wr_pos; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == BUSY_Pin) { AD7606_ReadAll(adc_buf[wr_idx][wr_pos]); if (++wr_pos >= FRAME_SIZE) { wr_idx ^= 1; /* 切换缓冲 */ wr_pos = 0; xSemaphoreGiveFromISR(frame_ready, NULL); /* 唤醒发送任务 */ } } }

要点是中断里只做三件事:读样本、推进写指针、必要时切换缓冲并给出信号量。任何 LwIP 调用都不能出现在中断里,尤其 netconn 系列的 API 会触发上下文切换,在中断里调用直接进 HardFault。高于 50kSPS 时逐样本读耗时偏长,应改为 DMA 批量搬移、DMA 完成中断里做缓冲切换,思路不变。

提示:adc_buf 和网卡 DMA 描述符都必须放在主 SRAM(0x20000000 起),不要图快把它们塞进 CCM RAM。F407 的 CCM 只能被内核访问,以太网 DMA 和通用 DMA 都读不到,放进去了表现是能编译、能运行,但收到的数据要么全零要么乱跳。

4.3 采样率预算:百兆链路下 AD7606 的实际上限

先按纯数据量算账:每次转换 16 字节,乘以采样率就是 8 通道总数据率。下表加上以太网头开销和 LwIP 的处理开销,才是工程可用的估算。

AD7606 采样率8 通道数据率占 100M 线速工程评估
10 kSPS160 KB/s1.3%轻松
50 kSPS800 KB/s6.4%常规
100 kSPS1.6 MB/s12.8%需调优
200 kSPS3.2 MB/s25.6%贴极限

链路本身在 100M 下有效载荷上限约 11.9MB/s,但瓶颈通常不在网线,而在 F407 内核、ETH DMA 描述符数量和 LwIP 拷贝次数。经验区间:LwIP + FreeRTOS 在 F407 上实测吞吐 3 到 5MB/s。100kSPS 以下按上面配置直接能跑;逼近 200kSPS 时要同时满足三条——FSMC 并口读取、乒乓缓冲用 DMA、PC 端 TCP 接收窗口开足够大,三者缺一都会先丢帧而不是先减速。

5. 断连、乱帧、速率上不去时,先查帧序号、MSS 和缓冲位置

5.1 用 Python 脚本统计帧序号缺口

上位机把收到的 seq 相邻差值统计一下,就能定位丢点究竟发生在采集侧还是网络侧。TCP 保证字节不丢,所以网络正常时 seq 只能连续,一旦出现缺口,说明采集端的缓冲切换或 BUSY 中断响应出了滞后。

import socket, struct cli = socket.create_connection(("192.168.1.10", 5001)) prev = None total = lost = 0 def recv_n(n): b = b"" while len(b) < n: b += cli.recv(n - len(b)) return b while True: head = recv_n(10) magic, seq = struct.unpack(">HH", head[:4]) if magic != 0xAA5A: print("sync lost at frame", total) break total += 1 if prev is not None: gap = (seq - prev - 1) & 0xFFFF if gap: lost += gap print("seq gap at frame", total, "lost", gap) prev = seq

如果 lost 持续增长但 Wireshark 里没有 TCP 重传,说明问题在采集侧,优先检查 BUSY 中断服务时长和 wr_pos 切换逻辑,而不是怀疑网卡。如果重传很多,再去查发送批量是不是太小、TCP_WND 是否被塞满。

5.2 先过 MTU 这一关再谈带宽

改任何 LwIP 参数之前,先验证大包通路。PC 端执行 ping,Windows 用ping 192.168.1.10 -l 1472 -f,Linux 用ping -M do -s 1472 192.168.1.10。1472 字节 ICMP 载荷加上 28 字节 IP 头正好 1500,如果这条命令报 fragmentation needed,说明链路上 MTU 不是 1500,TCP_MSS 再改也是白搭。

5.3 缓冲对齐与 CCM RAM 的最后一查

调速率最后还没解决时,回到缓冲区本身。网卡 DMA 要求描述符和缓冲区地址 4 字节对齐,用__attribute__((aligned(32)))声明即可;同时确认 lwipopts.h 的 MEM_ALIGNMENT 为 4。再查一遍有没有把缓冲声明到 CCM,或把 ETH DMA 描述符数组放在了.ccmram段,这两处是移植例程时最隐蔽的“能编能跑、数据全错”。

配套一个长期省力的做法:在 AD7606 的 CONVST 引脚同时挂一个硬件计数器,记录累计转换次数,与 PC 端收到的样本总数对账。两边数字一致说明全链路无丢失,不一致时用帧序号缺口出现的时刻反查中断耗时,多数情况下最终都收敛到 DataSetupTime 太紧或缓冲切换里多了一条耗时语句。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询