2、ECU硬件架构:MCU内部SPI/I2C外设模块、DMA与中断控制器、时钟树与总线矩阵
好,咱们进入第二章。这一章我打算把ECU的硬件底牌翻出来给你看看。说白了,SPI和I2C不是凭空跑起来的,它们得靠MCU内部一堆硬件模块伺候着。你如果只懂协议不懂硬件架构,那写出来的代码大概率是「能用,但不好用」。
2.1 MCU内部的SPI/I2C外设模块长什么样?
每个MCU厂商的SPI/I2C模块寄存器布局都不一样,但核心逻辑是相通的。我习惯把外设模块想象成一个「独立的小处理器」——它有自己的状态机、移位寄存器、数据缓冲区和控制逻辑。
SPI模块的核心组件:
- 移位寄存器(Shift Register):8位或16位,负责串并转换。发送和接收其实是同一个寄存器在干活,所以SPI是全双工的。
- 数据缓冲(TX/RX Buffer):一般是双缓冲设计。你写数据到TX Buffer,硬件自动移出去;RX Buffer收到数据后,你赶紧读走。
- 时钟分频器(Baud Rate Generator):从系统时钟分频得到SCK频率。我遇到过有人把分频系数设错了,导致SPI通信时好时坏,查了半天才发现是时钟太快了。
- 控制/状态寄存器:配置主从模式、极性相位、中断使能等。
I2C模块的核心组件:
- 地址比较器:从机模式下,硬件自动比对地址,匹配了才响应。
- 时钟同步与仲裁逻辑:多主机模式下,硬件帮你处理总线冲突。
- 数据缓冲(Data Register):同样是双缓冲,但I2C的字节传输有ACK/NACK机制,硬件会自动处理。
- 状态机(FSM):管理起始条件、地址发送、数据收发、停止条件等状态跳转。
重要提醒:很多MCU的SPI和I2C模块是共享引脚资源的。你选了SPI功能,I2C就不能用了。选型时一定要看引脚复用表,别等画完PCB才发现冲突。
2.2 DMA控制器:让CPU喘口气
没有DMA的时候,SPI每收发一个字节,CPU就得进一次中断。你想想看,如果SPI时钟是10MHz,每秒要处理1000万个字节,CPU光来回跑中断就累死了。
DMA(直接存储器访问)说白了就是一个「数据搬运工」。你告诉它:从外设数据寄存器搬100个字节到内存缓冲区,搬完了通知我。然后DMA自己干活,CPU该干嘛干嘛。
DMA在SPI/I2C中的典型配置:
- 源地址:外设数据寄存器地址(固定不变)
- 目的地址:内存缓冲区地址(递增)
- 传输长度:要传输的字节数
- 触发源:SPI/I2C的接收或发送请求信号
- 传输模式:单次传输或循环传输
我的经验:我曾经在一个项目中用DMA+SPI读取外部ADC数据。一开始没注意DMA的地址对齐问题,结果数据总是错位。后来发现,有些MCU要求DMA传输的源地址必须按字对齐,否则会触发总线错误。嗯,这个坑我踩过,你注意一下。
2.3 中断控制器:NVIC的优先级艺术
中断控制器(NVIC)是MCU的「交通警察」。SPI/I2C模块产生中断请求,NVIC决定先处理谁、能不能打断当前任务。
中断优先级分组:
- 抢占优先级:高优先级可以打断低优先级的中断服务程序
- 子优先级:同抢占优先级下,先响应子优先级高的
我建议你把SPI中断设为较高优先级,I2C设为中等。为什么?SPI通常用于高速数据流(比如传感器数据采集),延迟大了数据可能丢失。I2C常用于配置类操作(比如设置寄存器),慢一点没关系。
注意:千万别把所有外设中断都设成同一个优先级。否则一旦多个中断同时触发,系统会按硬件编号顺序响应,这完全不可控。我曾经见过一个同事,把所有中断优先级都设成0,结果系统响应完全随机,调试了三天才发现问题。
2.4 时钟树与总线矩阵:数据怎么跑到外设的?
时钟树决定了SPI/I2C模块能跑多快。总线矩阵决定了数据从CPU到外设要走哪条路。
时钟树的关键路径:
- 外部晶振(HSE/LSE)或内部RC振荡器(HSI/LSI)产生原始时钟
- PLL倍频得到系统主时钟(SYSCLK)
- AHB预分频器得到HCLK(给CPU、内存、DMA用)
- APB预分频器得到PCLK(给SPI/I2C等外设用)
这里有个容易忽略的点:SPI模块的时钟源是PCLK,但I2C模块的时钟源可能是PCLK也可能是独立的时钟。具体要看芯片手册。我习惯在初始化代码里先确认外设时钟是否使能了——很多人忘了开RCC时钟,结果外设怎么配都不工作。
总线矩阵的作用:
- CPU通过系统总线访问外设寄存器
- DMA通过DMA总线直接访问外设和内存
- 外设之间可以通过总线矩阵实现数据交换(比如SPI数据直接送到内存)
总线矩阵的设计决定了并发访问能力。比如CPU在跑代码的同时,DMA在搬运SPI数据,两者互不干扰。但如果CPU和DMA同时访问同一块内存区域,总线仲裁器会介入,其中一个要等一等。
核心要点:理解时钟树和总线矩阵,你才能回答「为什么我的SPI跑不到理论最高速率?」——可能是APB时钟不够快,也可能是总线带宽被其他外设占用了。
2.5 实战配置示例:SPI+DMA+中断的初始化流程
光说不练假把式。我贴一段实际项目中用过的初始化代码(基于STM32,但逻辑通用):
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2. 配置GPIO为复用功能
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置SPI
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
// 4. 配置DMA
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rxBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 100;
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel2, ENABLE);
// 5. 使能SPI的DMA请求
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
// 6. 配置中断(DMA传输完成中断)
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE);
这段代码的顺序是有讲究的:先开时钟,再配GPIO,然后配外设,最后配DMA和中断。我曾经试过先配DMA再开外设时钟,结果DMA配置写不进去——因为外设时钟没开,寄存器根本不可访问。
2.6 避坑指南:我踩过的几个硬件坑
- 坑一:DMA传输完成中断没清标志——中断服务程序里必须手动清除DMA中断标志位,否则会一直进中断。我刚开始做DMA时忘了这步,CPU直接卡死在中断里。
- 坑二:SPI的NSS引脚配置错误——软件模式下NSS引脚要配成GPIO输出,硬件模式下要配成复用功能。搞混了会导致片选信号异常。
- 坑三:I2C的时钟延展没处理——从机如果来不及处理数据,会拉低SCL时钟线。主机必须支持时钟延展,否则通信会超时。有些低端MCU的I2C模块不支持这个特性,选型时要注意。
- 坑四:总线矩阵的AHB/APB桥接延迟——CPU访问APB外设时,如果AHB时钟比APB快很多,会有等待周期。极端情况下,连续读写外设寄存器可能出错。我建议在两次寄存器操作之间加一个NOP指令。
好了,这一章的内容就到这儿。下一章我们会深入SPI的时序细节,包括模式选择、片选管理和多从机拓扑。到时候我会拿一个实际项目中的传感器读取案例来拆解,保证干货满满。