14 KiB
14 KiB
STM32F4-Base
项目概述
STM32F407ZGTx (Cortex-M4 FPU) 基础固件项目,集成 CH395F 以太网控制器 SPI 驱动、GD5F2GQ5UE SPI NAND Flash 存储(含 FlashDB KVDB/TSDB 数据库)和 TPAFE5160 16位8通道同步采样 ADC 并行接口驱动。
目录结构
STM32F4-Base/
├── Src/ # CubeMX 生成的外设初始化 + main
├── Inc/ # CubeMX 生成的头文件
├── Drivers/
│ ├── BSP/
│ │ ├── CH395F/ # CH395F 以太网芯片驱动(手写)
│ │ ├── NET/ # BSD Socket API 网络抽象层(手写)
│ │ │ ├── net_config.h # 网络配置宏定义
│ │ │ ├── net_types.h # 类型定义、地址结构、Socket 控制块
│ │ │ ├── net_socket.h # BSD Socket API 头文件
│ │ │ ├── net_socket.c # API 核心实现 + 状态机
│ │ │ ├── net_select.h # select/poll API 头文件
│ │ │ └── net_select.c # select/poll 实现
│ │ ├── GD5F2GQ5UE/ # GD5F2GQ5UE NAND Flash 驱动(手写)
│ │ │ ├── gd5f2gq5ue.h/c # 底层 SPI 驱动
│ │ │ ├── fal_flash_gd5f2gq5ue.c # FAL 设备适配层
│ │ │ ├── fal_cfg.h # FAL 设备表 + 分区表
│ │ │ └── fdb_cfg.h # FlashDB 功能配置
│ │ └── TPAFE5160/ # TPAFE5160 ADC 并行接口驱动(手写)
│ ├── STM32F4xx_HAL_Driver/ # ST HAL 库(CubeMX 生成)
│ └── CMSIS/ # ARM CMSIS(CubeMX 生成)
├── Lib/
│ └── FlashDB/ # FlashDB 数据库库(v2.2.99)
│ ├── src/ # FlashDB 核心源码
│ ├── inc/ # FlashDB 头文件
│ └── port/fal/ # FAL 抽象层
├── MDK-ARM/ # Keil MDK 工程文件
├── docs/ # 参考文档
└── STM32F407-Demo.ioc # STM32CubeMX 项目源文件
关键文件
| 路径 | 说明 |
|---|---|
Src/main.c |
程序入口,初始化序列及主循环 |
Drivers/BSP/CH395F/ch395f.c/h |
CH395F 以太网芯片 SPI 驱动 |
Drivers/BSP/NET/net_socket.c/h |
BSD Socket API 网络抽象层 |
Drivers/BSP/NET/net_select.c/h |
select/poll I/O 多路复用 |
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c/h |
GD5F2GQ5UE NAND Flash SPI 驱动 |
Drivers/BSP/GD5F2GQ5UE/fal_flash_gd5f2gq5ue.c |
FAL 设备适配层 |
Drivers/BSP/TPAFE5160/tpafe5160.c/h |
TPAFE5160 ADC 并行接口驱动 |
MDK-ARM/STM32F407-Demo.uvprojx |
Keil MDK 项目文件 |
STM32F407-Demo.ioc |
STM32CubeMX 项目源文件 |
构建
仅支持 Keil MDK-ARM v5 (ARMCC)。打开 MDK-ARM/STM32F407-Demo.uvprojx 编译。
- 编译器:ARMCC V5.06 update 7
- 优化等级:
-O4(项目级),spi.c/usart.c/ HAL 源文件使用-O0 - C 标准:C99
- 全局宏定义:
USE_HAL_DRIVER, STM32F407xx
命令行编译(Agent 使用)
修改代码后必须执行命令行编译验证,编译脚本位于 MDK-ARM/build.bat:
cmd /c "cd /d "工作目录" && build.bat"
实际执行示例(从项目根目录):
cmd /c "cd /d "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM" && build.bat"
编译结果判读:
- 退出码 0:无错误无警告,编译成功
- 退出码 1:有警告无错误,编译失败(警告不可接受,需修复)
- 退出码 2+:编译失败,需查看
MDK-ARM/build_log.txt定位错误
UV4.exe 路径: C:\Keil_v5\UV4\UV4.exe
工程文件: MDK-ARM/STM32F407-Demo.uvprojx
目标名称: STM32F407-Demo
硬件配置
- 主频: HSE 25MHz → PLL 168MHz (4/168/2)
- 6 个 LED: PC4, PC5, PB1, PB2, PF11, PF12(低电平点亮)
- CH395F: SPI2 (PB12 CS, PB13 SCK, PB14 SDO, PB15 SDI)
- GD5F2GQ5UE: SPI1 (PE0 CS, PB3 SCK, PB4 MISO, PB5 MOSI, PB8 WP, PE1 HOLD)
- TPAFE5160: 并行16位 (PG0-PG15 数据, PD3 RD, PD4 CONVST, PD7 BUSY, PD1 FRSTDATA, PF13-15 OS[2:0])
- USART1: PA9 TX, PA10 RX (115200bps)
启动顺序
HAL_Init() → SystemClock_Config() → MX_GPIO_Init() → MX_USART1_UART_Init() → MX_SPI2_Init() → MX_SPI1_Init() → gd5f2gq5ue_init() → fdb_kvdb_init()
代码规范
参考 嵌入式C语言代码规范(V1.0).md,关键要点:
- 缩进:4 空格,禁止 Tab
- 命名:小写字母+下划线;全局变量
g_前缀,静态s_,指针p_,数组a_ - 函数注释块需包含:函数功能、入口参数、返回值、限定条件、函数说明
- 大括号:K&R 风格(左大括号不换行)
- 文件头注释:模块名称、功能、平台、作者、日期、修改记录
- 头文件保护宏:
__MODULE_NAME_H格式,带extern "C"
注意
Inc/和Src/中 CubeMX 生成的文件带有USER CODE BEGIN/END标记,自定义代码应写在这些区域之间ch395f.c/h、gd5f2gq5ue.c/h、fal_flash_gd5f2gq5ue.c为纯手工代码,不受 CubeMX 保护tpafe5160.c/h为纯手工代码,不受 CubeMX 保护- CH395F 每次 SPI 事务需调用
ch395f_spi_begin()/ch395f_spi_end()包裹 - GD5F2GQ5UE 的
gd5f2gq5ue.c中声明了extern SPI_HandleTypeDef hspi1,需确保 SPI1 已初始化 - FlashDB 使用 FAL 模式,
fdb_cfg.h中定义FDB_USING_FAL_MODE,fal_cfg.h中定义分区表 - FlashDB 详细使用说明见
FlashDB使用说明.md sd2506.c/h为纯手工代码,不受 CubeMX 保护
CH395F 驱动关键点
- 初始化必须按手册9.2.1节顺序:
SET_MAC→SET_IP/GWIP/MASK→INIT_CH395→SET_PHY - IP/网关/掩码必须在
INIT_CH395之前设置,INIT 会读取并锁定当前寄存器值到协议栈,之后再设 IP 无效 SET_PHY必须在INIT_CH395之后,它会复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数CMD_PING_ENABLE不需要显式调用,INIT 后默认可用- 每次 SPI 事务需调用
ch395f_spi_begin()/ch395f_spi_end()包裹 - 大数据量收发使用 SPI2 DMA:
ch395f_write_send_buf()和ch395f_read_recv_buf()已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 - DMA 缓冲区:
s_spi2_dma_tx_buf[1500]/s_spi2_dma_rx_buf[1500],4 字节对齐 - TCP 重传参数:
SET_RETRAN_COUNT(默认12次,最大20)和SET_RETRAN_PERIOD(默认500ms,最大1000ms)必须在INIT_CH395之前设置,总重传时间 = 次数 × 周期 - KeepAlive 参数必须为 500ms 的倍数:
SET_KEEPALIVE_IDLE(默认20000ms)、SET_KEEPALIVE_INTVL(默认15000ms)单位为 ms,且 IDLE 必须 > INTVL,均为 500 的倍数;传入非 500 倍数的值会导致未定义行为(CH395F 内部定时器异常,TCP 连接几秒内 TIMEOUT) - KeepAlive 默认关闭:需在
SINT_STAT_CONNECT后调用ch395f_set_keepalive_enable(sock, 1)启用 - Socket 4-7 默认无收发缓冲:CH395F 默认只为 Socket 0-3 各分配 4 个缓冲区块(2048 字节)。多连接模式下使用 Socket 4-7 时,必须在
open_socket之前显式分配:ch395f_set_send_buf(sock, 28, 2)— 分配发送缓冲(例:块 28-29,共 1024 字节)ch395f_set_recv_buf(sock, 30, 2)— 分配接收缓冲(例:块 30-31,共 1024 字节)- 不分配会导致发送数据为固定垃圾内容(
0x0028填充)、接收缓冲区无法存储数据
- TCP 关闭重连的正确方法:直接调用
ch395f_close_socket(),不要先调ch395f_tcp_disconnect()- ❌
disconnect→close:disconnect发送 FIN 将 Socket 推入 FIN_WAIT_2,此后close不再发送 RST,Socket 卡在 FIN_WAIT_2 必须等远端发 FIN 才能关闭(可能需数分钟超时) - ✅
close直接:在 ESTABLISHED 或 CLOSE_WAIT 状态下调用close会发送 RST 立即终止连接,Socket 瞬间回到 CLOSED(已验证closed: sock=0x00 tcp=0x00) - 关闭后需轮询
ch395f_get_socket_status()等待sock=0x00再open_socket,典型耗时 < 1s
- ❌
- SOCK_TIMEOUT 中断:长时间无数据时 CH395F 可能触发
SINT_STAT_SOCK_TIMEOUT,不应将其视为致命错误——记录日志后继续操作即可,不要因此关闭 Socket - UDP 客户端/服务器模式(手册 §9.2.4):通过
SET_DES_IP_SN的 IP 地址区分:- DesIP=0xFFFFFFFF(255.255.255.255)→ UDP Server 模式:接受任意来源的数据,接收缓冲区中数据前 8 字节为信息头(
[0-1]reserved [2-3]src_port(LE) [4-7]src_ip [8+]payload),回发前需设置SET_DES_IP和SET_DES_PORT指定目标 - DesIP=具体 IP → UDP Client 模式:只接受指定 IP:Port 的数据,接收数据无信息头,只能向预设的目标发送
- DesIP=0xFFFFFFFF(255.255.255.255)→ UDP Server 模式:接受任意来源的数据,接收缓冲区中数据前 8 字节为信息头(
- UDP 发送缓冲必须等 SENDBUF_FREE(手册要求):每次
ch395f_write_send_buf()后必须等待SINT_STAT_SENBUF_FREE中断(0x01),否则下次写入会被 CH395F 静默丢弃。可通过轮询ch395f_get_sock_int_status()检查此标志
测试说明
- 测试代码文件:
Drivers/BSP/CH395F/ch395f_test.h/ch395f_test.c - PC 端测试脚本:
test/ch395f_socket_test.py,支持 Python 3.7+ - 调用入口:
Src/main.c的USER CODE BEGIN 2区域 - Phase 1(9 项寄存器测试):✅ 通过
- Phase 2(TCP Client 收发 + 关闭重连 ×3 轮):✅ 通过
- Phase 3(UDP Server Echo + PING + 大包):✅ 通过
- Phase 4(NET 层 TCP Echo):✅ 通过,主循环永久运行
- Phase 5(NET 层 UDP Echo):✅ 通过
- Phase 6(DHCP 自动获取 IP):✅ 通过
- Phase 7(Select/Poll I/O 多路复用):✅ 通过
- Phase 8(多客户端 7 并发):✅ 通过,成功率 100%
- 测试拓扑:PC (192.168.1.2) ↔ 交换机 ↔ CH395F (192.168.1.100)
- 详细使用说明:见
docs/CH395F_Test_Guide.md
GD5F2GQ5UE 驱动关键点
- SPI Mode 0(CPOL=0, CPHA=0),时钟 42MHz
- 初始化必须按顺序:复位 → 读 ID → 使能 ECC(B0h=10h)→ 解除块保护(A0h=00h)
- SET_FEATURE 命令前必须先发写使能(06h)
- 块擦除(D8h)参数是字节地址(块编号 × 128KB),不是块编号
- 读取 ID(9Fh)返回 3 字节,第 0 字节无意义,第 1 字节 MID,第 2 字节 DID
FlashDB 分区规划
| 分区名 | 偏移 | 大小 | 用途 |
|---|---|---|---|
| fdb_kvdb1 | 0 | 64MB | KVDB 键值数据库 |
| fdb_tsdb1 | 64MB | 64MB | TSDB 时序数据库 |
TPAFE5160 驱动关键点
- AD7606 P2P 兼容替代品,并行接口协议一致
- 并行模式:CS 接地(始终选中),PAR/SER/BYTE SEL 接 GND(并行),DB15/BYTE SEL 接 GND(非字节模式)
- 数据总线 DB[15:0] 接 GPIOG[15:0],通过
(uint16_t)GPIOG->IDR一次读取16位 - CONVST 上升沿触发全部8通道同步采样,BUSY 高电平表示转换中
- RD 下降沿输出通道数据,按通道1~8顺序依次输出
- FRSTDATA 在第一个 RD 下降沿变高,指示通道1数据就绪
- 过采样 OS[2:0] 在 BUSY 下降沿锁存,无过采样时 tCONV=1.74µs,64倍过采样时 tCONV=167µs
- 读取时序:168MHz 下 GPIO 写操作 + 5个 NOP (~30ns) 覆盖 t10=22ns 和 t14=21ns 要求
- 硬件 RANGE 接 GND → ±5V 量程,LSB=152.59µV
NET 网络层关键点
- 文件结构:
Drivers/BSP/NET/下包含 net_config.h、net_types.h、net_socket.h/c、net_select.h/c - 使用前必须调用
net_init(ip, mask, gateway)初始化网络子系统 - 主循环必须调用
net_poll()轮询所有 Socket 状态,处理中断事件 - TCP Server 多连接模式:1 个监听 Socket + 最多 7 个数据 Socket
- select/poll 支持:
net_select()和net_poll_events()用于 I/O 多路复用 - 事件回调:可通过
net_set_event_cb()注册连接/断开/数据到达等事件回调 - 非阻塞模式:
net_recv()使用NET_MSG_DONTWAIT标志,或使用 select/poll - 字节序转换:使用
net_htons()/net_ntohs()/net_htonl()/net_ntohl()进行主机序/网络序转换 - IP 地址转换:
net_inet_addr("192.168.1.100")字符串转网络序,net_inet_ntoa()反向转换 - 注意:新文件需手动添加到 Keil MDK 工程中才能编译
- RECV 中断是电平触发的:
net_poll()中do { ... } while(0)只处理一批中断,由主循环读取数据,避免无限循环导致主循环饿死 - TCP Server 多连接模式:Socket 0 专职监听,Socket 1~7 自动分配
- 多连接模式初始化顺序(关键!遗漏将导致 TCP 连接失败):
ch395f_set_fun_para(0x02)— 启用多连接(在INIT_CH395之前)ch395f_init()— 协议栈初始化- Socket 0:
SET_PROTO_TCP → SET_SOUR_PORT → OPEN → TCP_LISTEN - Socket 1~7:
SET_PROTO_TCP → SET_SOUR_PORT(相同端口)(不 OPEN!)
- 如果不配置数据 Socket 的协议类型和源端口,CH395F 不知道哪些 Socket 可用于多连接分配,客户端连接会被静默拒绝(无 CONNECT 中断)
- TCP Server 单连接模式:Socket 先
OPEN→SET_PROTO_TCP→SET_SOUR_PORT→OPEN→TCP_LISTEN;收到SINT_STAT_CONNECT后 Socket 自动切换为数据通道;DISCONNECT/TIMEOUT后需重新OPEN→TCP_LISTEN
已知问题
CH395F 与 RTL8305NBI 自动协商不兼容
CH395F 与 RTL8305NBI-CG 直连(经网络变压器)时,自动协商始终失败(返回 PHY_DISCONN),但强制 100M 全双工工作正常。强制 10M 全双工同样失败。
解决方案: 初始化协议栈后调用 ch395f_set_phy(CH395F_PHY_100M_FULL) 跳过自动协商。