4、开发环境搭建:Keil/IAR工程配置、STM32CubeMX初始化、LWIP协议栈移植、CAN驱动库集成

好,咱们进入第四章。这一章说白了就是“搭台子”。你想想看,后面所有的路由逻辑、协议转换,都得跑在这个台子上。台子搭不稳,后面全是白费功夫。

我个人习惯,做这种嵌入式网关项目,先把工具链理清楚。Keil 还是 IAR?STM32CubeMX 怎么配?LWIP 怎么塞进去?CAN 驱动从哪来?咱们一个一个来。

核心思路: 先有硬件抽象(CubeMX),再有中间件(LWIP + CAN),最后才是应用逻辑。顺序别搞反。

4.1 工具链选择:Keil MDK vs IAR EWARM

说实话,这两家我都用过。Keil 的社区资源多,网上例程一抓一把。IAR 的编译优化确实狠,代码密度能小 10%-15%。

我建议你根据团队习惯来选。如果项目就你一个人,用 Keil 省心。如果是团队协作,IAR 的工程管理更规范。

对比项 Keil MDK IAR EWARM
编译器 ARMCC v6 (基于 Clang) IAR C/C++ Compiler
调试器支持 J-Link, ST-Link, ULINK J-Link, I-jet, ST-Link
LWIP 集成 有 CMSIS-Driver 封装 需手动移植
代码优化 中等 高(尤其速度优化)
License 价格 约 $5000(Pro) 约 $8000(Pro)
我的经验: 如果你用 STM32H7 系列,IAR 的优化效果更明显。H7 的 Cache 和 TCM 配置,IAR 的链接脚本处理得更细腻。

4.2 STM32CubeMX 初始化配置

这一步是地基。我见过太多人上来就写代码,结果 Pin 脚冲突、时钟配错,折腾两天才发现是 CubeMX 没配好。

咱们这个网关,核心外设就这几个:ETH(以太网 MAC)、CAN(FD 或经典 CAN)、USART(调试用)、SPI(如果外接 CAN 控制器)。

  1. 时钟树配置: 外部晶振 25MHz,HSE 倍频到 400MHz(H7)或 216MHz(F4)。ETH 需要 50MHz 的 RMII 参考时钟。
  2. ETH 引脚: RMII 模式,PA1(REF_CLK),PG11(TX_EN),PG13(TX_D0),PG14(TX_D1),PA2(RX_D0),PC4(RX_D1),PA7(CRS_DV)。
  3. CAN 引脚: 以 STM32F407 为例,CAN1 用 PD0(RX)和 PD1(TX)。CAN2 用 PB5(RX)和 PB6(TX)。
  4. DMA 配置: ETH 的 DMA 描述符建议用 4 个 TX 和 4 个 RX。CAN 的 FIFO 用 DMA 传输,减轻 CPU 负担。
避坑: 我曾经在 F407 上配 RMII 时钟,忘了把 PA1 设为 AF11,结果 ETH 死活不通。查了两天手册才发现 REF_CLK 的复用功能号不对。嗯,这里要注意,每个型号的 AF 号不一样。

4.3 LWIP 协议栈移植

LWIP 移植,说白了就是让协议栈知道“怎么把数据塞进 ETH 的 DMA 描述符里”。

我习惯用 CMSIS-Driver 的方式移植。ST 官方已经封装好了 ETH 的 CMSIS 驱动,你只需要在 lwipopts.h 里配好参数。

// lwipopts.h 关键配置
#define NO_SYS                  1           // 无 RTOS 模式
#define LWIP_SOCKET             0           // 不用 Socket API
#define LWIP_NETCONN            0           // 不用 Netconn API
#define LWIP_HTTPD              1           // 启用 HTTP 服务器(用于 Web 配置)
#define MEM_SIZE                (16 * 1024) // 内存堆大小
#define PBUF_POOL_SIZE          16          // PBUF 池数量
#define PBUF_POOL_BUFSIZE       1536        // 每个 PBUF 大小(含以太网帧头)

移植的核心文件就三个:ethernetif.c(底层接口)、lwip.c(初始化)、lwipopts.h(配置)。

我建议你把 ethernetif.c 里的 low_level_output() 函数仔细看一遍。它负责把 LWIP 的 pbuf 链转换成 ETH 的 DMA 描述符链。这里容易出 Bug。

关键点: LWIP 的 pbuf 是链表结构,而 ETH DMA 描述符是数组结构。转换时要注意内存对齐和字节序。

4.4 CAN 驱动库集成

CAN 驱动,我分两种情况讲。

情况一:STM32 内置 CAN 控制器(bxCAN)

直接用 STM32 HAL 库的 CAN 驱动。初始化流程很简单:

// CAN 初始化示例
hcan1.Instance = CAN1;
hcan1.Init.Prescaler = 6;           // 分频系数
hcan1.Init.Mode = CAN_MODE_NORMAL;  // 正常模式
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_13TQ;
hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;
hcan1.Init.TimeTriggeredMode = DISABLE;
hcan1.Init.AutoBusOff = ENABLE;
hcan1.Init.AutoWakeUp = DISABLE;
hcan1.Init.AutoRetransmission = DISABLE;
hcan1.Init.ReceiveFifoLocked = DISABLE;
hcan1.Init.TransmitFifoPriority = DISABLE;

HAL_CAN_Init(&hcan1);

情况二:外接 SPI 接口的 CAN 控制器(如 MCP2515)

这种就需要自己写 SPI 读写函数。我建议你封装一个 can_spi_driver.c,把 SPI 的 CS、SCK、MOSI、MISO 操作抽象出来。

CAN 方案 优点 缺点
内置 bxCAN 集成度高,延迟低 FIFO 深度有限(3 个邮箱)
外接 MCP2515 FIFO 可扩展,支持 CAN FD SPI 通信有额外延迟
外接 SJA1000 经典方案,资料多 不支持 CAN FD,已停产
我的建议: 如果项目对实时性要求高(比如 1ms 以内的 CAN 响应),用内置 bxCAN。如果数据量很大(比如每秒上千帧),外接 MCP2515 配合 DMA 更稳。

4.5 整体架构图

下面这张图,是我做这个网关时的软件分层结构。你照着搭,基本不会错。

CAN转以太网网关 - 软件分层架构 应用层 路由引擎 | 协议转换 | Web 配置界面 | 日志管理 中间件层 LWIP 协议栈 | CANopen 协议栈 | 消息队列 | 定时器管理 硬件抽象层 (HAL) ETH DMA 驱动 | CAN 驱动 | SPI 驱动 | USART 调试驱动 硬件层 STM32F407/H7 | PHY (LAN8720A) | CAN 收发器 (TJA1050) | 电源管理 说明: 1. 应用层不直接操作硬件寄存器,全部通过 HAL 层访问。 2. LWIP 和 CANopen 运行在中间件层,通过消息队列与应用层通信。 3. 硬件抽象层由 STM32CubeMX 生成,但 CAN 驱动需要手动集成。

4.6 工程文件结构

最后,给你看看我习惯的工程目录结构。清晰的文件组织,能让你少掉不少头发。

Project_Gateway/
├── Core/
│   ├── Inc/           # 头文件
│   ├── Src/           # 主程序、中断、系统配置
│   └── Startup/       # 启动文件
├── Drivers/
│   ├── CMSIS/         # ARM CMSIS 核心
│   ├── STM32H7xx_HAL/ # STM32 HAL 库
│   └── BSP/           # 板级支持包(ETH PHY、CAN 收发器)
├── Middleware/
│   ├── LWIP/          # LWIP 协议栈源码
│   ├── CANopen/       # CANopen 协议栈(可选)
│   └── FreeRTOS/      # 实时操作系统(可选)
├── Application/
│   ├── can_route/     # CAN 路由引擎
│   ├── eth_route/     # 以太网路由引擎
│   ├── web_config/    # Web 配置页面
│   └── log/           # 日志管理
├── Tools/
│   ├── script/        # 编译脚本、烧录脚本
│   └── test/          # 单元测试代码
└── Doc/               # 设计文档、API 手册
注意: LWIP 的源码不要放在 HAL 库目录下。我之前见过有人把 LWIP 塞进 Drivers 文件夹,结果升级 HAL 库时把 LWIP 也覆盖了。嗯,血的教训。