3、MCU选型与架构:常见光模块MCU(STM32/EFM8/MSP430)、内部资源(Flash/RAM/定时器)、外设接口(I2C/SPI/UART/GPIO)

做光模块固件开发,选MCU是第一道坎。

说实话,市面上能用的MCU太多了,但真正适合光模块的,翻来覆去就那么几款。我这些年经手过的项目,从10G到400G,主控芯片基本没跳出过STM32、EFM8和MSP430这三个家族。今天咱们就掰开揉碎聊聊,这三款芯片到底该怎么选,内部资源怎么用,外设接口怎么搭。

3.1 三大主流MCU:谁是你的菜?

先看一张对比表,心里有个底:

特性 STM32(F0/F1/L0系列) EFM8(Silicon Labs) MSP430(TI)
内核 Cortex-M0/M3 8051(增强型) 16位RISC
主频 48-72MHz 24-50MHz 16-25MHz
Flash 16-256KB 8-64KB 1-128KB
RAM 4-32KB 0.5-4KB 0.5-8KB
功耗 中等(μA级待机) 低(nA级待机) 极低(nA级待机)
开发难度 中等(HAL库) 简单(寄存器操作) 中等(DriverLib)
典型光模块 100G/400G DSP方案 SFP+/QSFP+ 低速模块 低功耗/工业级模块

STM32——我最常用的主力。Cortex-M0内核,性能够用,外设丰富。做100G以上模块时,DSP芯片需要配合一个主控MCU做管理,STM32F0系列是我的首选。为什么?因为它的I2C接口支持400kHz快速模式,处理DDM数据时不会卡顿。

EFM8——老朋友了。8051内核的现代版,Silicon Labs专门为光模块优化的。我记得2018年做SFP28项目时,EFM8BB10F8G这颗芯片,8KB Flash、0.5KB RAM,硬是跑完了完整的SFF-8472协议栈。它的优势是:外设集成度高,内部有精密振荡器,省掉外部晶振,BOM成本能降0.3美金。

MSP430——低功耗之王。TI的16位架构,待机电流低到0.1μA。我有个客户做工业级光模块,要求-40°C到85°C全温范围,功耗不能超过50mW。MSP430G2553配合LDO,整板功耗控制在35mW,稳得很。

选型口诀:

  • 要性能、要生态 → STM32
  • 要成本、要简单 → EFM8
  • 要低功耗、要稳定 → MSP430

3.2 内部资源:Flash、RAM、定时器怎么分配?

光模块固件对资源的要求,其实挺苛刻的。你想想看,一个模块里要跑I2C从机协议、DDM监控、激光器控制、告警处理,还得留空间给升级功能。资源分配不好,后面全是坑。

3.2.1 Flash:代码和数据的家

Flash主要放三样东西:程序代码、常量表、校准参数。

  • 程序代码:一般占60%-70%。我习惯把协议栈(SFF-8472/8636)放在固定区域,方便后续升级。
  • 常量表:比如温度补偿表、APC曲线表。这些表用const声明,放在Flash里,不占RAM。
  • 校准参数:每个模块出厂时都要写校准值。注意!校准参数必须放在独立的Flash页里,方便擦写。我曾经遇到过,把校准参数和代码混在一起,结果升级固件时把校准值也擦掉了,整批模块报废……

避坑指南:

我曾经在STM32F030上做批量升级,发现Flash擦写次数超过10万次后,某些页会失效。后来我改用双备份机制——主区和备份区各存一份校准参数,写之前先校验,写之后回读确认。从此再没出过问题。

3.2.2 RAM:寸土寸金

光模块的RAM通常很小,EFM8才0.5KB,MSP430也就2KB。怎么省着用?

  • 全局变量:能省则省。我习惯把所有状态标志位用bit field打包,一个字节存8个标志。
  • 堆栈:这是最容易爆的地方。中断嵌套深了,栈溢出直接死机。我建议中断服务函数里只设标志位,具体处理放到主循环。
  • DMA缓冲区:I2C和SPI的收发缓冲区,尽量复用。比如发送缓冲区和接收缓冲区共用一块内存,用乒乓操作。
// 示例:EFM8上RAM极度紧张时的变量打包技巧
// 一个字节存8个状态标志
typedef union {
    uint8_t all;
    struct {
        uint8_t tx_ready : 1;
        uint8_t rx_pending : 1;
        uint8_t alarm_los : 1;
        uint8_t alarm_tx_fault : 1;
        uint8_t ddm_updated : 1;
        uint8_t fw_updating : 1;
        uint8_t reserved1 : 1;
        uint8_t reserved2 : 1;
    } bits;
} StatusFlags_t;

// 全局只用一个字节
StatusFlags_t g_status = {0};

3.2.3 定时器:光模块的节拍器

定时器在光模块里太重要了。我一般至少用三个:

  • 定时器0:1ms系统滴答,用于DDM数据采集。每1ms读一次ADC,更新温度、电压、偏置电流、发射功率。
  • 定时器1:100ms轮询,用于告警检测。比如LOS(信号丢失)状态,100ms扫一次就够了。
  • 定时器2:PWM输出,控制激光器的偏置电流。这个频率要高,一般用20kHz以上,避免人眼可见的闪烁。

我的习惯:

定时器中断里不要做复杂运算。我见过有人把温度补偿算法放在定时器中断里,结果中断执行时间超过定时周期,系统直接卡死。正确的做法是:中断里只设标志,主循环里处理。

3.3 外设接口:I2C、SPI、UART、GPIO

光模块和外界通信,全靠这几个接口。每个接口都有它的脾气。

3.3.1 I2C:光模块的命脉

I2C是光模块最重要的接口。主机(交换机)通过I2C读取模块的DDM信息,写入配置参数。标准速率100kHz,快速模式400kHz。

我踩过最大的坑是I2C时钟拉伸。有些从机芯片(比如DSP)会拉低SCL线,要求主机等待。如果MCU的I2C外设不支持时钟拉伸,通信就会超时。STM32的I2C外设默认支持,但EFM8的某些型号需要软件模拟。

// 示例:STM32 I2C初始化(快速模式400kHz)
void I2C_Init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    
    // 配置GPIO:PB6-SCL, PB7-SDA
    GPIOB->AFR[0] |= (4 << 24) | (4 << 28);  // AF4
    GPIOB->MODER |= (2 << 12) | (2 << 14);  // Alternate function
    
    // I2C时序配置:PCLK=48MHz,目标400kHz
    I2C1->TIMINGR = 0x00201D2B;  // 这个值要根据时钟算
    I2C1->CR1 |= I2C_CR1_PE;     // 使能I2C
}

3.3.2 SPI:高速数据通道

SPI在光模块里主要用于和DSP芯片通信。速率可以跑到10MHz以上,比I2C快得多。但SPI的缺点是线多——至少4根(SCK、MOSI、MISO、CS)。

我个人习惯用SPI的DMA模式。比如从DSP读取眼图数据,一次要传几千个字节,用CPU轮询太浪费。DMA传输时CPU可以干别的事,比如处理告警。

SPI vs I2C 怎么选?

  • 需要高速批量数据 → SPI(比如读DSP寄存器、下载固件)
  • 需要多从机、线少 → I2C(比如读温度传感器、EEPROM)
  • 引脚不够用 → I2C(只用2根线)

3.3.3 UART:调试利器

UART在量产模块里一般不启用,但开发阶段必不可少。我每个项目都会预留一个UART口,用115200波特率,输出调试信息。

这里有个技巧:用UART实现命令行交互。我写过一个简单的shell,支持set/get命令,调试时直接敲命令读写寄存器,比用烧录器方便多了。

// 伪代码:UART命令行解析
void UART_ProcessCmd(char *cmd) {
    if (strncmp(cmd, "get", 3) == 0) {
        uint8_t addr = (uint8_t)atoi(cmd + 4);
        uint8_t val = ReadRegister(addr);
        printf("Reg[0x%02X] = 0x%02X\r\n", addr, val);
    } else if (strncmp(cmd, "set", 3) == 0) {
        uint8_t addr = (uint8_t)atoi(cmd + 4);
        uint8_t val = (uint8_t)atoi(cmd + 7);
        WriteRegister(addr, val);
        printf("Set OK\r\n");
    }
}

3.3.4 GPIO:最灵活的外设

GPIO看似简单,但用好了能解决大问题。光模块里GPIO的典型用途:

  • TX_DISABLE:输出,控制激光器关断。上电时必须拉高,防止模块未初始化时激光器误亮。
  • TX_FAULT:输入,检测激光器故障。我习惯用外部中断触发,一旦拉低立即关断激光器。
  • LOS:输入,检测信号丢失。这个信号需要去抖,一般用定时器延时10ms再确认。
  • MOD_DEF0:I2C地址选择。通过GPIO电平决定模块的I2C地址是0xA0还是0xA2。

注意:

GPIO的上下拉电阻一定要配好。我曾经在EFM8上遇到TX_FAULT引脚悬空,结果上电时电平不确定,导致激光器误关断。后来我在外部加了10kΩ上拉电阻,问题解决。

3.4 知识体系总览

下面这张图,把MCU选型、内部资源、外设接口的关系串起来了。你看完应该能对光模块的MCU架构有个整体认识。

光模块MCU选型与架构总览 STM32系列 EFM8系列 MSP430系列 内部资源 Flash(代码+校准) RAM(变量+堆栈) 定时器(PWM/滴答) ADC(DDM采集) 外设接口 I2C(DDM/配置) SPI(DSP通信) UART(调试) GPIO(控制/告警) 选型关键:性能匹配、资源够用、接口齐全、成本可控

嗯,MCU选型这块,说到底就是平衡的艺术。性能高了浪费,低了不够用。我建议新手先从EFM8入手,寄存器少,上手快。等把光模块协议跑通了,再换STM32做复杂项目。至于MSP430,如果你做的是工业级或者电池供电的模块,它会是你的好搭档。

下一节咱们聊固件架构设计,我会分享一个我自己用了五年的分层架构模板,保证代码好维护、好移植。


专注资料整理