AVR USI模块SPI通信配置详解:从寄存器操作到实战调试
1. 项目缘起:为什么还要折腾AVR的USI SPI?
最近在整理一个老项目的维护文档,又翻出了几块ATtiny85和ATtiny13的开发板。这些小家伙现在看起来性能平平无奇,但在一些对成本、功耗和体积极其敏感的场合,比如简单的传感器节点、一次性消费电子产品或者作为大系统中的协处理器,它们依然是极具性价比的选择。在调试一个基于ATtiny85的无线模块通信时,我再次用到了它的USI(Universal Serial Interface)模块来实现SPI通信。说实话,比起STM32、ESP32这些现代MCU丰富的外设库,AVR的USI配置起来确实需要多花点心思,寄存器直接操作的感觉很“复古”,但也正因为如此,一旦吃透,你对SPI时序的理解会深刻得多。
网上关于AVR USI SPI的资料,要么是年代久远的代码片段语焉不详,要么是直接给个“标准配置”却不说为什么。新手照着配,通信不通是常态,然后就开始怀疑人生。其实,USI模块本身设计得很灵活,但它的灵活性也带来了配置的复杂性。SPI通信的核心在于主从设备间时钟和数据线的严格同步,而USI模块需要你手动“组装”出这个同步逻辑。这就像给你一套乐高零件(移位寄存器、时钟逻辑),告诉你它能拼出一辆车(SPI),但具体怎么拼,轮子装哪,得你自己看说明书(数据手册)并动手试。
所以,这篇内容不是简单的代码搬运,而是结合我多次调试的经验,带你从USI模块的底层逻辑出发,一步步拆解如何将它配置成SPI主机或从机。我们会搞清楚每个寄存器位的作用,弄明白时钟极性和相位(CPOL/CPHA)到底在配置什么,并给出经过实测的驱动代码框架。无论你是正在学习AVR,还是在维护老项目,希望这篇“踩坑总结”能帮你少走弯路。
2. USI模块的本质:一个灵活的串行接口“积木盒”
在深入SPI配置之前,我们必须先理解USI到底是什么。它不是ATmega或ATtiny系列里那种独立的、功能固定的SPI外设(像ATmega16的SPI模块)。USI,顾名思义,是一个通用串行接口。你可以把它想象成一个高度可配置的数字积木盒,里面核心包含了一个8位移位寄存器、一个时钟发生器/选择器和一些控制逻辑。
它的“通用”性体现在,通过不同的配置,这个模块可以模拟出多种串行协议:
- 三线模式(USIWM[1:0] = 0b01): 可以用于自定义的、简单的双向数据线通信。
- I2C模式(USIWM[1:0] = 0b10): 需要配合外部上拉电阻,实现I2C主从机功能。
- SPI模式(USIWM[1:0] = 0b11 或 0b00?这里有个关键点): 这就是我们今天的重点。但请注意,数据手册上SPI模式的设置值可能因型号略有差异,需要查证。
对于SPI通信,USI模块主要提供了以下硬件支持:
- 移位寄存器(USIDR): 这是数据进出的核心。当你写入数据到USIDR,在时钟驱动下,数据会从
DO(或DI,取决于模式)引脚一位一位地移出;同时,对方发送的数据也会一位一位地从DI引脚移入到USIDR中。 - 时钟单元: 这是最容易出错的地方。USI的时钟可以来自三个地方:
- 软件触发(USICLK): 你写1到
USICLK位来产生一个时钟脉冲。这在低速或需要精确控制时序时有用。 - 外部时钟(USCK/SCK引脚): 当配置为从机时,时钟由外部主机提供。
- 定时器/计数器0比较匹配: 可以产生一个固定频率的时钟,用于主机模式。 你需要通过
USICS[1:0]和USICLK等位来选择合适的时钟源和边沿。
- 软件触发(USICLK): 你写1到
- 控制逻辑: 包括计数器(USICNT[3:0])用于计数移位的位数(SPI通常是8位),以及一些状态标志位(如移位完成标志
USIOIF)。
关键理解: USI模块不自动处理SPI通信中的“片选(SS)信号”。片选需要你手动控制一个普通的GPIO引脚。这是与全功能SPI外设的一个重要区别。同时,它通常只支持全双工或半双工的SPI模式(即同时收发),像单线双向模式需要更复杂的软件模拟。
注意: 不同AVR型号的USI可能略有差异。例如,ATtiny85的USI功能比ATtiny13更完整。务必以你手头芯片的官方数据手册(Datasheet)为准,本文以ATtiny85/45/25系列为主要参考。
3. 核心配置详解:从寄存器位到SPI时序
配置USI为SPI模式,本质上是告诉它:“请把你的移位寄存器和时钟逻辑,按照SPI协议的规则来运作”。这主要通过USICR(USI控制寄存器)和USISR(USI状态寄存器)来完成。我们结合SPI的四个关键参数来理解:
SPI模式(时钟极性CPOL与时钟相位CPHA): 这是SPI配置的基石,决定了时钟空闲状态和数据的采样时刻。
- CPOL=0: 时钟线(SCK)空闲时为低电平。
- CPOL=1: 时钟线空闲时为高电平。
- CPHA=0: 数据在时钟的第一个边沿(如果CPOL=0,就是上升沿;CPOL=1,就是下降沿)被采样。
- CPHA=1: 数据在时钟的第二个边沿被采样。
对于USI,你需要通过USICLK(控制是否产生时钟脉冲)和USICS[1:0]/USIWM[1:0]的配合来模拟这些边沿。一个常见的对应关系是(以主机模式为例):
- 模式0 (CPOL=0, CPHA=0): 空闲低电平,数据在上升沿采样。配置USI在时钟上升沿移位(可能需要设置
USICSx选择外部时钟上升沿触发,或配合软件控制)。 - 模式3 (CPOL=1, CPHA=1): 空闲高电平,数据在下降沿采样。配置USI在时钟下降沿移位。
具体配置步骤拆解:
3.1 确定主从模式与引脚映射
首先,通过USIWM[1:0]位设置SPI模式。对于ATtiny85,设置USIWM1=1, USIWM0=1通常用于SPI主机或从机(具体行为还需结合时钟源设置)。
然后,查看数据手册的“引脚配置”章节,找到USI功能对应的物理引脚。以ATtiny85为例:
PB0(XCK/TO/DI) 通常作为数据输入DI。PB1(T1/DO) 通常作为数据输出DO。PB2(AIN1/SCK) 通常作为时钟线SCK。- 片选
SS你需要自己指定一个GPIO,比如PB3。
你需要将这些引脚配置为正确的方向(输入或输出)。主机模式下,DO和SCK应设置为输出,DI为输入。从机模式下,DO为输出,DI和SCK为输入。
// ATtiny85 SPI主机引脚初始化示例 #define SPI_DDR DDRB #define SPI_PORT PORTB #define SPI_PIN PINB #define USI_DI PB0 // 输入 #define USI_DO PB1 // 输出 #define USI_SCK PB2 // 输出 #define SPI_SS PB3 // 片选,普通GPIO输出 void spi_master_init(void) { // 配置DO, SCK, SS 为输出 SPI_DDR |= (1 << USI_DO) | (1 << USI_SCK) | (1 << SPI_SS); // 配置DI为输入(通常内部上拉可开可不开) SPI_DDR &= ~(1 << USI_DI); // 初始时,片选拉高(无效),时钟线设置为空闲状态(取决于CPOL) SPI_PORT |= (1 << SPI_SS); // 如果CPOL=0,SCK初始低;CPOL=1,SCK初始高。这里以模式0为例: SPI_PORT &= ~(1 << USI_SCK); // 空闲低电平 }3.2 配置时钟源与边沿(实现CPOL/CPHA)
这是最核心的一步,通过USICR寄存器配置。我们以实现主机模式、SPI模式0为例:
void usi_spi_master_mode0_init(void) { // 设置USI为三线模式(SPI的一种实现方式),实际上ATtiny85的SPI模式可能对应特定的USIWM值 // 根据数据手册,对于SPI主机,通常设置 USIWM1=1, USIWM0=0 或 其他组合,务必查表! // 假设我们查表得知:USIWM[1:0]=0b00 为SPI主机,使用软件时钟或定时器 // USICS[1:0]=0b00 表示时钟源来自软件触发 // USICLK 位用于在软件中产生时钟边沿 // 但我们更常用的是使用定时器作为时钟源,以产生稳定的时钟。 // 例如,设置 USICS1=1, USICS0=0 表示时钟源来自定时器0比较匹配。 // 同时,设置 USICLK=0(不立即产生时钟),USIOIE=0(禁用溢出中断)。 // 以下是一个常见的、经过简化的主机模式0初始化代码框架: USICR = 0; // 先清零 // 设置SPI模式:USIWM1=1, USIWM0=0 (示例,需核对) // 设置时钟源:USICS1=1, USICS0=0 (Timer0 Compare Match) // 不使能时钟输出和中断 USICR = (1 << USIWM1) | (1 << USIWM0) | (1 << USICS1); // 再次强调,此值为示例 // 同时,需要配置Timer0来产生合适的比较匹配频率,以决定SPI的时钟速度。 // 例如,设置CTC模式,设置OCR0A的值。 TCCR0A = (1 << WGM01); // CTC模式 TCCR0B = (1 << CS01); // 预分频8 OCR0A = 255; // 比较值,决定频率 F_SPI = F_CPU / (2 * prescaler * (1 + OCR0A)) // 注意:USI会在每次Timer0比较匹配时自动产生一个时钟脉冲,其边沿由USI的配置决定。 }关键点:USICS[1:0]和USICLK位的组合,共同决定了移位寄存器在哪个时钟边沿动作。对于模式0(CPOL=0, CPHA=0),我们需要数据在上升沿被从机采样,同时主机也在上升沿采样输入数据(对于全双工)。这意味着USI应该在时钟的某个边沿(可能是上升沿)触发一次移位操作。你需要仔细阅读数据手册中关于“时钟输出”和“移位寄存器时钟”的时序图,来确定如何设置才能产生符合SPI模式要求的边沿序列。
实操心得:很多时候通信失败,就是因为这个边沿没设对。一个笨但有效的方法是,用逻辑分析仪或示波器抓取SCK、DO、DI的波形,然后对照SPI时序图,看数据采样点是否正确。如果发现数据错位,尝试调整
USICSx的设置,或者改变你手动产生时钟(如果使用软件时钟)的顺序。
3.3 编写数据收发函数
配置好硬件后,数据收发就围绕着USIDR寄存器和计数器USICNT展开了。基本流程如下:
- 将待发送的数据写入
USIDR。 - 清除计数器(
USICNT[3:0]=0)或设置计数器为需要移位的位数(对于8位数据,可以写入0,因为计数器溢出值是0,移位8次后溢出;或者直接设置USICNT[3:0]=0b1000,但不同芯片处理方式不同)。 - 启动传输。如果是软件时钟,则需要循环产生时钟脉冲(写
USICLK位);如果使用定时器时钟,则使能定时器或等待传输完成。 - 等待传输完成标志
USIOIF置位。 - 从
USIDR中读取接收到的数据。
// 一个使用软件时钟(手动翻转SCK)的SPI主机发送/接收函数示例(模式0) uint8_t usi_spi_transfer(uint8_t data) { USIDR = data; // 加载要发送的数据 USISR = (1 << USIOIF); // 清除溢出中断标志,同时将计数器清零(通过写1来清除标志) // 手动产生8个时钟脉冲(软件时钟模式) // 这种模式下,我们需要自己控制SCK引脚的高低变化来满足CPOL和CPHA // 以下代码模拟模式0:空闲低电平,数据在上升沿采样 for (uint8_t i = 0; i < 8; i++) { // 先设置数据位(DO)稳定 // 然后产生上升沿(SCK从低到高) SPI_PORT |= (1 << USI_SCK); // SCK拉高,产生上升沿 _delay_us(1); // 短暂延时,确保建立时间 // 在这里,从机将在上升沿采样数据位 // 主机也需要在上升沿采样输入数据(但USI硬件可能自动在某个边沿锁存DI) // 实际上,对于软件模拟,我们通常在时钟边沿后读取数据 // 更准确的做法是依赖USI硬件移位,我们只触发时钟: // USICR |= (1 << USICLK); // 产生一个时钟脉冲(如果配置为软件时钟触发) // while (!(USISR & (1 << USIOIF))); // 等待一次移位完成 SPI_PORT &= ~(1 << USI_SCK); // SCK拉低,产生下降沿(为下一个上升沿准备) _delay_us(1); } // 传输完成后,USIDR中已经是接收到的数据 // 但更标准的做法是等待USIOIF标志,然后读取USIDR // while (!(USISR & (1 << USIOIF))); // 等待8次移位完成 return USIDR; }重要提示:上面的for循环示例是纯软件模拟SPI时序,并没有充分利用USI的硬件移位功能,仅用于理解时序。在实际使用USI硬件时,我们更倾向于配置好时钟源后,启动传输,然后等待USIOIF标志。下面的代码更接近硬件辅助的写法:
uint8_t usi_spi_hw_transfer(uint8_t data) { USIDR = data; // 数据放入移位寄存器 USISR = (1 << USIOIF); // 写1清除溢出标志,并复位计数器为0 // 启动传输:如果是软件时钟模式,循环触发USICLK;如果是定时器时钟,使能定时器或等待。 // 假设我们配置为使用定时器0比较匹配作为时钟源(自动产生时钟) // 那么只需要等待传输完成即可。 // 但为了通用性,这里展示一种常见的“等待循环触发”方式(适用于多种时钟源配置): // 通过一个do-while循环,等待计数器溢出(USIOIF置位) // 在循环内,如果配置为软件时钟,可能需要手动触发USICLK。 // 以下是一个简化的模式: do { // 如果时钟源是软件触发,则需要在此执行:USICR |= (1 << USICLK); // 对于已配置为外部或定时器时钟的情况,这行代码可能不需要。 // 我们使用一个空语句或极短延时,让硬件自动工作。 _delay_us(0.1); } while (!(USISR & (1 << USIOIF))); // 等待8位数据移位完成 // 传输完成,USIDR中现在是从设备读回的数据 return USIDR; }4. 从机模式配置要点与常见问题排查
将USI配置为SPI从机,逻辑上更简单,因为时钟(SCK)完全由外部主机控制。但也有一些需要特别注意的地方。
4.1 从机模式初始化
- 引脚配置:
DI和SCK设置为输入,DO设置为输出。特别注意,DO引脚通常需要在USIDR被写入数据后,才能输出有效数据。有些配置下,需要设置USIWM和USICS为从机模式,使得DO引脚在移位过程中自动输出。 - 寄存器配置: 设置
USIWM[1:0]为SPI从机模式(查数据手册确定值)。设置USICS[1:0]选择外部SCK引脚作为时钟源,并配置在正确的边沿(与主机模式匹配)。例如,对于模式0,从机需要在SCK的上升沿采样数据,所以USI应该配置为在SCK上升沿触发移位。 - 片选(SS)处理: 从机必须有一个片选引脚(通常是一个普通GPIO配置为输入)。当
SS被主机拉低时,从机开始准备或响应通信。在一些严格的SPI实现中,SS引脚还用于复位从机的内部状态(如移位计数器)。在USI中,你可以将SS引脚的变化与外部中断或引脚变化中断结合,来初始化USI或准备数据。
// ATtiny85 SPI从机初始化示例(模式0) void spi_slave_init(void) { // 配置DI, SCK为输入,DO为输出 DDRB &= ~((1 << USI_DI) | (1 << USI_SCK)); // DI, SCK 输入 DDRB |= (1 << USI_DO); // DO 输出 // 可选:使能内部上拉,防止悬空 PORTB |= (1 << USI_DI) | (1 << USI_SCK); // 配置USI为SPI从机模式,使用外部SCK时钟,在上升沿触发 // 假设查表得:USIWM[1:0]=0b11 为SPI从机,USICS[1:0]=0b01 为外部时钟正边沿 USICR = (1 << USIWM1) | (1 << USIWM0) | (1 << USICS0); // 清除标志和计数器 USISR = (1 << USIOIF); }4.2 从机数据收发
从机的数据收发通常由中断驱动。你可以使能USI溢出中断(USIOIE),当8位数据移位完成(计数器溢出)时,进入中断服务程序(ISR)。在ISR中,读取USIDR得到主机发来的数据,同时将需要回复给主机的下一个数据写入USIDR。
// 全局变量用于数据交换 volatile uint8_t spi_received_data = 0; volatile uint8_t spi_data_to_send = 0xFF; // 默认发送0xFF // USI溢出中断服务程序 ISR(USI_OVF_vect) { spi_received_data = USIDR; // 读取接收到的数据 USIDR = spi_data_to_send; // 装入要发送的数据(为下一次传输准备) USISR |= (1 << USIOIF); // 清除溢出标志(通过写1),并复位计数器 // 注意:清除标志后,USI会等待下一个SCK时钟边沿继续移位。 } void spi_slave_enable(void) { USICR |= (1 << USIOIE); // 使能USI溢出中断 sei(); // 开启全局中断 // 预先加载第一个要发送的数据 USIDR = spi_data_to_send; USISR = (1 << USIOIF); // 清除标志,准备开始 }4.3 通信失败排查清单(主机/从机通用)
当你按照上述步骤配置后,如果SPI通信仍然失败,可以按照以下清单逐步排查:
物理连接:
- 检查
VCC和GND是否连接正确、牢固。 - 检查
SCK,DO,DI三条线是否交叉连接(主机的DO接从机的DI,主机的DI接从机的DO)。 - 片选
SS线是否连接并正确控制?主机在通信前拉低对应从机的SS,通信后拉高。 - 线路是否过长?是否有干扰?对于高速SPI,需要考虑信号完整性。
- 检查
电源与电平:
- 主从设备是否共地?这是必须的。
- 双方IO电平是否兼容?如果主机是5V,从机是3.3V,可能需要电平转换。
软件配置:
- CPOL和CPHA是否匹配?这是最常见的问题。用逻辑分析仪抓取
SCK和DO波形,对照SPI模式图检查。主机和从机的模式必须完全一致。 - 时钟频率是否过高?尤其是使用软件模拟时钟或低速MCU时,降低时钟频率(增加
_delay_us或调整定时器分频)试试。 - USI寄存器配置是否正确?反复核对
USICR和USISR的每一位,特别是USIWM[1:0]、USICS[1:0]和USICLK。最可靠的方法是,在数据手册中找到SPI通信的示例代码或时序图,对照着配置。 - 引脚方向(DDRx)设置是否正确?主机
SCK、DO输出,DI输入;从机反之。 - 是否在传输开始前正确加载了要发送的数据到
USIDR? - 是否清除了
USIOIF标志并复位了计数器?在每次传输开始前,通常需要写USISR来清除标志和复位计数器。 - 是否等待传输完成?在读取
USIDR之前,必须确保USIOIF标志置位(或使用中断)。
- CPOL和CPHA是否匹配?这是最常见的问题。用逻辑分析仪抓取
工具辅助:
- 逻辑分析仪是你的最佳朋友。一个几十块钱的简易逻辑分析仪(配合Sigrok/PulseView软件)就能清晰地显示
SCK、DO、DI、SS四条线上的时序,一眼就能看出数据在哪一位、时钟边沿是否正确。 - 没有逻辑分析仪,可以用两个LED分别接在
DO和SCK上,通过LED的闪烁情况粗略判断是否有数据在传输。
- 逻辑分析仪是你的最佳朋友。一个几十块钱的简易逻辑分析仪(配合Sigrok/PulseView软件)就能清晰地显示
5. 进阶话题:性能优化与特殊场景处理
在基本通信调通后,你可能会考虑以下问题:
5.1 提高SPI通信速度
USI的SPI速度受限于几个因素:
- CPU主频: 这是上限。
- 时钟源: 使用定时器比较匹配产生的时钟,比软件翻转
SCK引脚要快得多,也稳定得多。 - 代码效率: 传输函数的循环、延时、标志检查都会消耗时间。尽量使用中断和DMA(如果支持)来解放CPU。
- 对于主机,可以尝试在
while等待USIOIF时,并行处理其他不冲突的任务。 - 对于从机,中断服务程序(ISR)应尽可能短小精悍,只做必要的数据搬运。
- 对于主机,可以尝试在
计算SPI时钟频率: 如果使用定时器0比较匹配作为时钟源(USICS1=1, USICS0=0),则SPI时钟频率为:F_SPI = F_CPU / (2 * N * (1 + OCR0A))其中,N是定时器预分频系数(1, 8, 64, 256, 1024)。例如,F_CPU=8MHz,预分频N=8,OCR0A=0,则F_SPI = 8MHz / (2*8*1) = 500kHz。设置OCR0A=1,则F_SPI = 8MHz / (2*8*2) = 250kHz。
5.2 处理多从机与大数据量传输
- 多从机: 每个从机都需要独立的
SS片选线。主机在通信前,只拉低目标从机的SS,其他保持高电平。USI模块本身不管理SS,这需要你在软件中精确控制GPIO。 - 大数据量传输: 连续发送多个字节时,需要注意
SS信号的控制。通常,在一次“事务”中(比如读写一个传感器的多个寄存器),SS应始终保持低电平。在字节与字节之间,你需要确保USI已经完成前一个字节的传输(USIOIF置位),然后立即写入下一个字节到USIDR,并清除标志启动下一次传输。避免在字节间产生不必要的SCK空闲周期,除非协议要求。
5.3 USI与其他功能引脚的冲突
在ATtiny等小引脚芯片上,USI引脚(DI,DO,SCK)可能与ADC输入、外部中断、PWM输出等功能复用。如果你同时需要使用这些功能,必须在初始化时规划好引脚功能。通过DDRx、PORTx寄存器以及相关功能模块(如ADC、定时器)的使能位来协调。通常,一个引脚在同一时刻只能用于一种主要功能。
6. 一个完整的、可移植的USI SPI驱动框架示例
下面提供一个针对ATtiny85/45/25的、相对完整的SPI主机驱动框架,采用定时器时钟源,模式0。请注意,其中的寄存器位定义需要根据你使用的具体芯片和编译器(如AVR-GCC)进行调整。
/** * USI SPI Master Driver for ATtiny85/45/25 * Mode: 0 (CPOL=0, CPHA=0) * Clock Source: Timer0 Compare Match */ #include <avr/io.h> #include <util/delay.h> // 引脚定义 #define SPI_DDR DDRB #define SPI_PORT PORTB #define SPI_PIN PINB #define USI_DI_PIN PB0 #define USI_DO_PIN PB1 #define USI_SCK_PIN PB2 #define SPI_SS_PIN PB3 // 用户自定义片选 // USI寄存器位定义(ATtiny85示例,请核对你的芯片头文件) #ifndef USIWM1 #define USIWM1 USIWM0 // 有时位定义名称不同,需要适配 #endif // ... 其他位定义 USIWM0, USICS1, USICS0, USICLK, USIOIE等 /** * @brief 初始化USI为SPI主机模式0 * @param clock_divider 时钟分频因子,影响SPI速度。值越大,速度越慢。 * 实际计算需参考数据手册和Timer0配置。 */ void usi_spi_master_init(uint8_t clock_divider) { // 1. 配置引脚方向 SPI_DDR |= (1 << USI_DO_PIN) | (1 << USI_SCK_PIN) | (1 << SPI_SS_PIN); SPI_DDR &= ~(1 << USI_DI_PIN); // DI 输入 // 2. 设置初始电平:SS高(无效),SCK低(模式0空闲低) SPI_PORT |= (1 << SPI_SS_PIN); SPI_PORT &= ~(1 << USI_SCK_PIN); // 3. 配置Timer0用于产生SPI时钟 // 使用CTC模式,比较匹配时触发USI时钟 TCCR0A = (1 << WGM01); // CTC模式 // 设置预分频和比较值。这里简化处理,clock_divider用于设置OCR0A // 更精细的控制需要根据F_CPU计算 TCCR0B = (1 << CS01); // 预分频8 OCR0A = clock_divider; // 比较值,控制频率 // 4. 配置USI控制寄存器 // 设置SPI主机模式,使用Timer0比较匹配作为时钟源 USICR = (1 << USIWM1) | (1 << USIWM0) // SPI Master模式 (请核对!) | (1 << USICS1); // Clock Source = Timer0 Compare Match // 5. 清除状态标志和计数器 USISR = (1 << USIOIF); } /** * @brief 选择从设备(拉低片选) */ void spi_select_slave(void) { SPI_PORT &= ~(1 << SPI_SS_PIN); _delay_us(1); // 短暂延时,确保从设备识别到片选变化 } /** * @brief 取消选择从设备(拉高片选) */ void spi_deselect_slave(void) { _delay_us(1); // 短暂延时,确保最后一位数据被处理 SPI_PORT |= (1 << SPI_SS_PIN); } /** * @brief 交换一个字节数据(发送并接收) * @param data 要发送的字节 * @return 接收到的字节 */ uint8_t usi_spi_transfer_byte(uint8_t data) { USIDR = data; // 加载发送数据 USISR = (1 << USIOIF); // 清除溢出标志,复位计数器 // 等待传输完成(8个时钟脉冲由Timer0自动产生) // 这里用while循环等待标志位,也可以考虑用中断提高效率 while (!(USISR & (1 << USIOIF))) { // 空循环,等待。如果长时间等不到,应考虑超时处理。 } return USIDR; // 返回接收到的数据 } /** * @brief 发送/接收多个字节(数据块) * @param tx_buf 发送数据缓冲区指针,如果为NULL,则发送0xFF * @param rx_buf 接收数据缓冲区指针,如果为NULL,则忽略接收的数据 * @param len 数据长度 */ void usi_spi_transfer_block(const uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) { for (uint16_t i = 0; i < len; i++) { uint8_t tx_data = (tx_buf != NULL) ? tx_buf[i] : 0xFF; uint8_t rx_data = usi_spi_transfer_byte(tx_data); if (rx_buf != NULL) { rx_buf[i] = rx_data; } } } // 示例:主函数中读取一个SPI设备(例如,一个SPI Flash的ID) int main(void) { usi_spi_master_init(255); // 初始化SPI,设置较低的时钟速度 uint8_t cmd = 0x9F; // 读ID命令 uint8_t id_buf[3] = {0}; spi_select_slave(); usi_spi_transfer_byte(cmd); // 发送命令 usi_spi_transfer_block(NULL, id_buf, 3); // 读取3字节ID,发送dummy数据(0xFF) spi_deselect_slave(); // 此时id_buf中包含了设备ID // ... 其他操作 while (1) { // 主循环 } return 0; }这个框架提供了初始化和基础的单字节/多字节传输函数。在实际项目中,你需要根据连接的从设备的具体协议(如命令格式、等待时间、CRC校验等)在这个基础上进行封装。
最后,调试USI SPI是一个需要耐心和细致观察的过程。从理解寄存器每一位开始,用逻辑分析仪验证每一个时序,遇到问题就对照数据手册和排查清单。一旦跑通,你会发现这个看似简单的“积木盒”其实非常强大可靠,足以应对许多嵌入式场景中的串行通信需求。
