4、开发环境搭建:Keil/IAR环境配置、FreeRTOS移植、LWIP协议栈移植、CAN驱动框架搭建

好,咱们正式开始动手了。这一章,说白了就是搭积木。你得先把开发环境、操作系统、网络协议栈、CAN驱动这几块“积木”准备好,后面的网关才能跑起来。我见过不少新手,一上来就急着写应用代码,结果环境没配好,光编译错误就折腾了两天。嗯,咱们别走弯路。

4.1 Keil/IAR环境配置——选顺手的那把刀

我个人习惯用Keil,但IAR也有它的铁粉。其实两者差别不大,核心就是把芯片包装好、编译器选对。

Keil环境配置要点:

  • 安装MDK-ARM,版本建议5.36以上,对Cortex-M4/M7支持更好。
  • 通过Pack Installer安装对应MCU的Device Family Pack。比如STM32F4系列,就装Keil.STM32F4xx_DFP.2.x.x。
  • 在Target选项卡里,把ARM Compiler选成V6.x(AC6)。为什么?AC5虽然稳定,但AC6编译速度更快,代码密度也更好。我在项目中遇到过AC5编译一个稍大的工程要40秒,换成AC6后直接降到15秒。

IAR环境配置要点:

  • 安装IAR Embedded Workbench for ARM,版本8.50以上。
  • 在Project -> Options -> General Options里,选对Device型号。
  • 在C/C++ Compiler -> Preprocessor里,添加你的头文件路径。这个步骤容易漏,我曾经因为少加了一个路径,排查了整整一个下午。
我的小建议: 如果你团队里有人用Keil有人用IAR,最好统一一下。不然光是工程文件格式转换就够你喝一壶的。我个人偏向Keil,因为它的调试界面更直观。

4.2 FreeRTOS移植——让芯片学会“多任务”

网关要同时处理CAN报文和以太网数据,裸机跑肯定不行。FreeRTOS就是来干这个的。移植其实不复杂,核心就三步:改时钟、配堆栈、调中断。

第一步:获取源码

去FreeRTOS官网下载V10.4.x版本。别下最新的,稳定第一。把Source文件夹里的所有.c和.h文件复制到你的工程里。

第二步:配置FreeRTOSConfig.h

这个文件是FreeRTOS的灵魂。我直接给你看一个我常用的配置模板:

#define configUSE_PREEMPTION            1
#define configCPU_CLOCK_HZ              ( ( unsigned long ) 168000000 )  // 根据你的MCU主频改
#define configTICK_RATE_HZ              ( ( TickType_t ) 1000 )         // 1ms一个tick
#define configMAX_PRIORITIES            ( 5 )                           // 优先级够用就行,别贪多
#define configMINIMAL_STACK_SIZE        ( ( unsigned short ) 128 )      // 最小栈,单位是word
#define configTOTAL_HEAP_SIZE           ( ( size_t ) 64 * 1024 )       // 64KB堆,看你的RAM大小
#define configUSE_IDLE_HOOK             0
#define configUSE_TICK_HOOK             0
注意: configCPU_CLOCK_HZ一定要和你的系统时钟一致。我曾经在一个项目里配错了,结果任务调度时间全乱套,LED闪烁频率忽快忽慢,查了三天才发现是这里的问题。

第三步:实现滴答定时器

FreeRTOS需要一个硬件定时器来产生系统心跳。通常用SysTick。在stm32f4xx_it.c里加上:

void SysTick_Handler(void)
{
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
    {
        xPortSysTickHandler();
    }
}

嗯,这里要注意:必须在调度器启动后才能调用xPortSysTickHandler,否则会死机。

4.3 LWIP协议栈移植——给网关装上“网线”

LWIP是轻量级TCP/IP协议栈,名字就叫“轻量IP”。移植它,说白了就是告诉它:你的网卡怎么收发数据、你的时钟是多少。

核心文件修改:

  • lwipopts.h:配置LWIP功能开关。比如要不要支持UDP、TCP、DHCP。对于CAN网关,我建议打开UDP和DHCP,TCP看需求。
  • ethernetif.c:这是网卡驱动接口。你需要实现low_level_output(发送)和low_level_input(接收)两个函数。

给你看一个low_level_output的简化版:

static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
    struct pbuf *q;
    uint8_t *buffer;
    // 申请发送缓冲区
    buffer = (uint8_t *)malloc(p->tot_len);
    if (buffer == NULL) return ERR_MEM;
    // 将pbuf链表数据拷贝到连续缓冲区
    for (q = p; q != NULL; q = q->next)
    {
        memcpy(buffer, q->payload, q->len);
        buffer += q->len;
    }
    // 调用MAC层发送函数
    HAL_ETH_Transmit(&heth, (uint8_t *)buffer, p->tot_len, 1000);
    free(buffer);
    return ERR_OK;
}
避坑指南: 我曾经在移植LWIP时,发现网络ping不通。排查到最后,原来是ethernetif.c里的PHY地址写错了。不同厂家的PHY地址不一样,比如LAN8720是0x00,而DP83848是0x01。一定要看你的原理图!

4.4 CAN驱动框架搭建——打通“车联网”的任督二脉

CAN驱动是网关和汽车总线对话的桥梁。框架搭建得好,后面加CANopen、J1939协议栈就轻松了。

驱动分层设计:

我习惯把CAN驱动分成三层:

  1. 硬件抽象层(HAL):直接操作寄存器或HAL库函数。比如CAN初始化、发送、接收中断。
  2. 驱动层(Driver):封装成标准接口,比如CAN_SendMsg、CAN_ReceiveMsg。这里做数据校验和过滤。
  3. 应用层接口(API):给上层协议栈调用,比如CAN_Open、CAN_Close、CAN_Write。

初始化代码示例:

void CAN_Init(uint32_t baudrate)
{
    CAN_HandleTypeDef hcan;
    hcan.Instance = CAN1;
    hcan.Init.Prescaler = 6;          // 分频系数,根据baudrate计算
    hcan.Init.Mode = CAN_MODE_NORMAL; // 正常模式,调试时可以用LOOPBACK
    hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
    hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
    hcan.Init.TimeSeg2 = CAN_BS2_2TQ;
    hcan.Init.TimeTriggeredMode = DISABLE;
    hcan.Init.AutoBusOff = ENABLE;    // 自动恢复,省心
    hcan.Init.AutoWakeUp = ENABLE;
    hcan.Init.AutoRetransmission = ENABLE; // 自动重发,CAN总线冲突时有用
    hcan.Init.ReceiveFifoLocked = DISABLE;
    hcan.Init.TransmitFifoPriority = DISABLE;
    HAL_CAN_Init(&hcan);
}
我的经验: 调试CAN驱动时,先用LOOPBACK模式(回环模式)。这样你发出去的报文自己就能收到,不用接外部CAN设备。等回环测试通过了,再切到NORMAL模式。这个习惯帮我省了不少排查时间。

接收中断处理:

CAN报文接收建议用中断方式,别用轮询。网关要处理的事情多,轮询太浪费CPU了。中断里只做一件事:把报文放到队列里,然后通知FreeRTOS的任务去处理。

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData);
    // 将报文放入队列
    xQueueSend(canRxQueue, &rxData, 0);
}

你看,这样设计,CAN驱动和上层应用就解耦了。后面你想加CANopen协议栈,直接调用CAN_Write和CAN_Read就行,驱动层不用动。

好了,环境搭建就到这里。下一章咱们开始写网关的核心逻辑——CAN报文和以太网帧的互相转换。到时候你会发现,前面这些基础工作做得越扎实,后面写代码就越顺手。