EM773 SPI与定时器实战:Microwire协议与PWM生成详解

EM773 SPI与定时器实战:Microwire协议与PWM生成详解

1. 项目概述与核心价值

在嵌入式开发领域,尤其是基于NXP EM773这类微控制器的项目中,与外设进行可靠、高效的通信以及实现精准的定时控制,是两项最基础也最核心的任务。前者通常由SPI(Serial Peripheral Interface)这类串行总线协议承担,而后者则依赖于芯片内置的定时器/计数器模块。你可能已经无数次地在数据手册里见过SPI和定时器的章节,但当你真正需要驱动一个特殊的传感器,或者生成一组精确的PWM波形时,才会发现手册里那些时序图和寄存器描述背后,藏着许多决定成败的细节。

这次,我们就以EM773微控制器为蓝本,深入它的SPI0(带SSP功能)模块和16/32位定时器。我们不仅要看懂手册,更要弄明白在实际编程中,如何让SPI支持像Microwire这样的特殊帧格式,以及如何将定时器的匹配、捕获功能玩出花样,最终稳定地输出PWM。我会结合自己调试这类外设时踩过的坑,把寄存器配置背后的逻辑、时序上的微妙要求,以及代码编写时的注意事项掰开揉碎讲清楚。无论你是刚开始接触EM773,还是正在为某个外设的异常时序头疼,这篇文章都能给你提供一套可直接落地的思路和避坑指南。

2. EM773 SPI0 with SSP模块深度解析

EM773的SPI0模块并非一个简单的标准SPI控制器,它集成了SSP(Synchronous Serial Port)的特性,使其协议支持能力更加灵活。这意味着它不仅能处理标准的全双工SPI通信,还能适配像Microwire这样的半双工变种协议。理解这一点,是正确配置和使用该模块的前提。

2.1 SPI基础与SSP增强特性回顾

标准的SPI通信是一种全双工、同步的串行通信方式,通常包含四根线:

  • SCK (Serial Clock): 串行时钟,由主设备产生。
  • MOSI (Master Out Slave In): 主设备数据输出,从设备数据输入。
  • MISO (Master In Slave Out): 主设备数据输入,从设备数据输出。
  • SSEL (Slave Select): 从设备片选,低电平有效。

通信以数据帧为单位进行,时钟极性(CPOL)和时钟相位(CPHA)共同定义了数据采样和驱动的边沿。EM773的SPI0模块通过SSP框架,增强了对帧格式、数据长度(4-16位)和FIFO缓冲的支持,为兼容更多串行协议打下了基础。

注意:在配置任何通信外设前,务必先通过系统时钟控制寄存器(如SYSAHBCLKCTRL)打开对应模块的时钟源,并通过I/O配置寄存器(IOCONFIG)将相关引脚功能复用到SPI模式。这是很多初学者容易遗漏的第一步,导致读写寄存器无反应或引脚无输出。

2.2 Microwire协议帧格式与EM773的实现机制

Microwire协议可以看作是SPI的一个子集或变体,其最大特点是半双工基于命令-响应的通信模型。这对于许多需要先发送一个控制字节(如寄存器地址、读/写命令),再接收或发送数据的从设备(如某些EEPROM、温度传感器)来说非常高效。

根据手册描述,EM773的SPI0模块对Microwire帧格式的支持体现在以下几个关键时序和行为上:

2.2.1 单次传输帧格式一次完整的Microwire单帧传输包含两个阶段:

  1. 命令阶段 (8-bit Control Word): 主设备(EM773)通过SO线向从设备发送一个8位的控制字。在此期间,SI线被置为高阻态(三态),主设备不接收任何数据。这意味着在硬件上,你需要将MISO引脚配置为输入模式,并且软件上在这个阶段忽略接收到的数据(通常为无效值)。
  2. 响应阶段 (4 to 16 bits Data): 在8位控制字发送完毕,并经过一个SCK时钟的等待状态后,从设备开始将响应数据通过SI线发回。主设备在SCK的上升沿锁存数据。响应数据的长度可以是4到16位,因此整个帧长度在13到25位之间。

关键时序细节

  • 空闲状态: CS为高电平,SCK强制为低电平,SO强制为低电平。这个特性很重要,它定义了通信线的初始状态。
  • 传输启动: 当主设备将控制字节写入发送FIFO后,CS线的下降沿会触发传输。发送移位寄存器加载FIFO底部的数据,并从最高位(MSB)开始移出。
  • 传输结束: 对于单次传输,在最后一个数据位被接收移位寄存器锁存后,再过一个SCK周期,CS线会被拉高。这个上升沿会导致接收到的数据被传输到接收FIFO中,并且从设备可以释放SI线。

2.2.2 连续背靠背传输连续传输模式用于需要快速读写多个数据字的场景。其与单次传输的核心区别在于CS线的状态

  • 在连续传输中,CS线在整个多帧传输期间始终保持低电平(有效状态)。
  • 下一帧的8位控制字,紧接在当前帧响应数据的最后一位(LSB)之后立即开始发送,中间没有CS线的高电平间隔。
  • 每一帧接收到的数据,都会在其LSB被锁存后的SCK下降沿,从接收移位寄存器传输到接收FIFO。

这种模式极大地提高了连续读写的效率,但要求主从设备双方都能正确处理这种无间隔的帧衔接。

2.3 关键配置与实操要点

要让EM773的SPI0工作在Microwire模式,并稳定通信,你需要关注以下几个超越基础配置的要点:

1. 时钟与数据相位配置: 根据手册中的时序图,在Microwire模式下:

  • 命令发送阶段: 从设备在SCK上升沿锁存控制位。这对应SPI模式(CPOL, CPHA) = (0, 0)或(1, 1),具体取决于SCK空闲电平。由于手册指出空闲时SCK为低,因此通常采用模式(0,0):即SCK空闲为低,数据在上升沿采样。
  • 数据接收阶段: 从设备在SCK下降沿驱动数据,主设备在SCK上升沿采样。这依然符合模式(0,0)的规则。

2. 片选(CS)信号的建立与保持时间: 这是Microwire模式下极易出错的点。手册特别强调了当SCK自由运行时(即主设备持续提供时钟),CS信号相对于SCK上升沿的时序要求。

  • 建立时间(t_SETUP): 在采样第一个接收数据位的SCK上升沿之前,CS信号必须已经保持低电平至少2个SCK周期
  • 保持时间(t_HOLD): 在采样第一个接收数据位的SCK上升沿的前一个SCK上升沿之后,CS信号必须保持低电平至少1个SCK周期

这意味着什么?你不能简单地在使能SPI模块的同时拉低CS。一个稳妥的软件操作顺序是:

  1. 配置SPI模块为主机模式,设置好时钟频率、数据位宽(设为8位,对应控制字阶段)和帧格式。
  2. 将CS引脚配置为GPIO并手动控制。
  3. 在启动传输前,先手动拉低CS引脚。
  4. 等待足够的时间(通常通过几个NOP指令或短延时函数),以满足2个SCK周期的建立时间要求。这个延时时间需要根据你的SCK频率计算。例如,若SCK=1MHz,周期为1μs,则至少需要延时2μs。
  5. 将控制字写入SPI数据寄存器,启动传输。
  6. 在传输结束后(可通过查询状态寄存器或中断),再手动拉高CS引脚。

3. 数据长度与FIFO操作:

  • Microwire帧的总长度是可变的。在EM773中,你需要通过寄存器设置数据帧长度。对于“8位控制字+N位数据”的帧,你需要将数据长度设置为总位数(8+N)。模块会连续发送/接收这么多位。
  • 充分利用发送和接收FIFO。在连续传输模式下,你可以预先向发送FIFO写入多个控制字,模块会自动处理帧与帧之间的衔接。同样,需要及时从接收FIFO中读取数据,避免溢出。

实操心得:调试Microwire设备时,逻辑分析仪是你的最佳伙伴。务必同时捕获CS、SCK、SO、SI四根线,对照手册的时序图逐一检查:CS的建立/保持时间、控制字和数据位的对应关系、等待状态是否满足。很多通信失败都是因为CS时序或数据长度设置错误导致的。

3. EM773 16/32位定时器模块应用实战

定时器是嵌入式系统的“心跳”。EM773提供了16位(CT16B0)和32位(CT32B0/1)两种定时器,其架构和功能高度相似,主要区别在于计数器的宽度,决定了最大定时周期。我们以CT16B0为例进行深入剖析,其原理完全适用于32位定时器。

3.1 定时器核心架构与工作模式

从框图看,EM773的定时器是一个结构清晰、功能强大的模块。其核心是一个Timer Counter,它会在Prescale Counter达到预设值时递增。这种预分频器结构允许你在定时器分辨率和溢出周期之间取得平衡。

3.1.1 定时器模式 vs. 计数器模式这是两个基本工作模式,由CTCR寄存器控制:

  • 定时器模式: TC在每个PCLK(外设时钟)的上升沿递增(实际上是在预分频器溢出后)。这是最常用的模式,用于产生精确的时间间隔。
  • 计数器模式: TC在外部捕获引脚(CT16B0_CAP0)的边沿(上升、下降或双边沿)上递增。这可以用来测量外部脉冲的频率或数量。

重要提示:当选择计数器模式时,外部输入信号的频率不能超过PCLK频率的一半。因为模块需要两个连续的PCLK上升沿来检测一次CAP输入的电平变化。例如,如果PCLK=50MHz,则外部计数信号频率需低于25MHz。

3.1.2 预分频器(PR)的精确计算预分频器是控制定时精度的关键。TC递增的实际频率是:F_tc = F_pclk / (PR + 1)

  • PR = 0时,TC每个PCLK周期加1,分辨率最高,但溢出最快。
  • PR = 65535(16位最大值)时,TC每65536个PCLK周期才加1,定时周期最长,但分辨率降低。

举例:假设系统PCLK为50MHz,我们需要一个10ms的定时中断。

  1. 先确定TC的计数频率。10ms = 0.01s,如果让TC直接计数到某个值MR后产生中断,则MR / F_tc = 0.01
  2. 为了获得合适的MR值(不宜过小,以减小误差),我们假设设置PR = 4999。则F_tc = 50MHz / (4999+1) = 10kHz。即TC每0.1ms加1。
  3. 那么,MR = 0.01s / 0.0001s = 100。将匹配寄存器MR0设置为100,并配置为匹配时复位TC并产生中断,即可得到精确的10ms定时。

3.2 匹配(Match)功能:定时器的灵魂

匹配功能是定时器最强大的特性之一。四个匹配寄存器(MR0-MR3)可以独立编程,当TC的值与某个MRn的值相等时,可以触发三种动作(通过MCR寄存器配置):

  1. 产生中断:通知CPU处理定时事件。
  2. 复位TC:让定时器重新从0开始计数,用于产生周期性信号。
  3. 停止TC和PC:暂停定时器,用于单次定时。

3.2.1 生成PWM波形PWM是匹配功能的典型应用。EM773支持单边沿控制的PWM输出。配置步骤如下:

  1. 选择PWM周期:选择一个匹配寄存器(通常用没有引脚输出的MR3)来设定PWM周期。将其值设置为PWM周期对应的计数值,并在MCR寄存器中使能“匹配时复位TC”(MR3R=1)。这样,每当TC计数到MR3的值时,就会清零重新开始,形成一个周期。
  2. 配置PWM占空比:使用其他匹配寄存器(MR0,MR1,MR2,它们对应外部输出引脚MAT0-MAT2)来设置占空比。在PWMCON寄存器中使能对应通道的PWM模式。
  3. 理解PWM规则
    • 每个PWM周期开始时,所有PWM输出为低电平(除非其匹配值设为0)。
    • TC计数到某个通道的匹配值时,该通道的PWM输出变为高电平。
    • TCMR3复位时(周期结束),所有高电平的PWM输出被拉低。
    • 因此,PWM占空比 =MRn/MR3。例如,MR3=1000,MR0=300,则MAT0输出占空比为30%的PWM波。

避坑指南:务必注意,用于设置占空比的匹配寄存器(MR0-MR2),其MCR中的“复位”和“停止”位必须设为0。只有用于设定周期的那个匹配寄存器(如MR3)才需要使能“复位”功能。如果设置错误,PWM将无法正常产生周期信号。

3.3 捕获(Capture)功能:测量外部信号

捕获功能用于“抓拍”外部事件发生时的精确时刻。当配置的捕获引脚(CT16B0_CAP0)上发生指定的边沿(上升、下降或双边沿)时,当前TC的值会被瞬间锁存到对应的捕获寄存器(CR0)中,并可选择产生中断。

典型应用——测量脉冲宽度

  1. 配置捕获控制寄存器CCR,使能在CAP0引脚上捕获上升沿和下降沿,并都产生中断。
  2. 在上升沿中断服务程序中,读取CR0的值,记为t1
  3. 在下降沿中断服务程序中,再次读取CR0的值,记为t2
  4. 脉冲高电平宽度 =(t2 - t1) * (PR+1) / F_pclk

注意事项:捕获功能依赖于TC的计数。如果TC在两次捕获之间发生了溢出(从最大值翻转到0),那么简单的t2-t1计算就会出错。在编写代码时,需要引入一个溢出计数器来扩展定时器的有效位数,或者在每次捕获后结合TC的溢出中断来修正计算。

3.4 外部匹配输出

当不用于PWM时,匹配功能可以直接控制引脚输出,实现精确的数字波形生成。通过EMR寄存器,可以配置当匹配发生时,对应的MAT引脚执行以下四种操作之一:置低、置高、翻转、无操作。

应用场景:生成一个精确的方波。假设我们需要一个频率为F_pclk/(2*(MR+1))的方波。

  1. MR0设置为目标计数值。
  2. MCR中配置为“匹配时复位TC”(MR0R=1)。
  3. EMR中配置MAT0为“匹配时翻转”(EMC0=11)。
  4. 这样,每当TC计数到MR0时,TC复位,同时MAT0引脚电平翻转,从而产生一个50%占空比的方波。

4. 系统集成与编程实践

理解了各个模块后,如何将它们集成到一个实际项目中,并写出稳定可靠的驱动代码,是更大的挑战。

4.1 驱动层设计要点

1. 初始化序列: 任何外设的初始化都必须遵循严格的顺序,对于EM773的SPI和定时器,一个稳健的序列如下:

// 以SPI0和CT16B0为例 void Peripherals_Init(void) { // 1. 使能外设时钟 (SYSAHBCLKCTRL) SYS->AHBCLKCTRL |= (1 << 7); // 使能CT16B0时钟 SYS->AHBCLKCTRL |= (1 << 11); // 使能SPI0时钟 (假设位11,需查手册确认) // 2. 配置I/O引脚功能 (IOCONFIG) // 将PIO0_x配置为SPI0_SCK, SPI0_MOSI, SPI0_MISO, SPI0_SSEL // 将PIO0_y配置为CT16B0_MAT0, CT16B0_CAP0等 IOCON->PIO0_x = ...; // 具体配置值参考数据手册引脚复用表 // 3. 外设模块具体配置 SPI0_Init(); TIMER16B0_Init(); } void SPI0_Init(void) { // 配置SPI0为Microwire主机模式,时钟极性相位,数据长度等 SPI0->CR0 = ...; // 控制寄存器0,设置数据长度、帧格式等 SPI0->CR1 = ...; // 控制寄存器1,设置主从模式、使能等 SPI0->CPSR = ...; // 时钟预分频,设置SCK频率 // 注意:SSEL引脚可能需要配置为GPIO并手动控制 } void TIMER16B0_Init(void) { // 停止定时器 TIMER16B0->TCR = 0; // 设置预分频器PR TIMER16B0->PR = 4999; // 例如,产生10kHz的TC计数频率 // 设置匹配寄存器MR3为PWM周期 TIMER16B0->MR3 = 1000; // PWM周期 = 1000 * (1/10kHz) = 100ms // 设置匹配寄存器MR0为PWM占空比 TIMER16B0->MR0 = 300; // 占空比30% // 配置匹配控制寄存器MCR TIMER16B0->MCR = (1 << 10); // MR3匹配时复位TC (MR3R=1) // 注意:MR0的MCR位保持为0(不中断,不复位,不停止) // 配置PWM控制寄存器PWMCON TIMER16B0->PWMCON = (1 << 0); // 使能MAT0为PWM输出 // 配置外部匹配寄存器EMR(对于PWM模式,此寄存器影响较小,但可初始化) TIMER16B0->EMR = 0; // 启动定时器 TIMER16B0->TCR = 1; }

2. 中断服务程序优化: 定时器的匹配、捕获中断和SPI的发送/接收完成中断是实时系统的关键。编写ISR时务必遵循:

  • 快进快出:ISR中只做最必要的操作(如设置标志、拷贝数据),繁重的处理放到主循环中。
  • 清除中断标志:在退出ISR前,必须向中断寄存器(如TMR16B0IR)的对应位写1来清除中断标志,否则会连续触发中断。
  • 防止重入:对于可能被频繁触发的中断,考虑使用临界区保护或确保ISR执行时间远小于中断间隔。

4.2 调试技巧与常见问题排查

问题1:SPI通信无反应,或数据错误。

  • 检查时钟和电源:确认SYSAHBCLKCTRL寄存器中对应位已置1。
  • 检查引脚复用:用逻辑分析仪或示波器检查SCK、MOSI引脚是否有输出。如果没有,首先检查IOCONFIG寄存器配置是否正确。
  • 检查CS时序:对于Microwire,用逻辑分析仪测量CS下降沿到第一个SCK上升沿的时间,是否满足至少2个SCK周期的建立时间要求。
  • 检查数据位序:确认主从设备的数据传输位序(MSB first / LSB first)是否一致。EM773的SPI模块通常可配置。
  • 检查FIFO:在连续传输时,确保在发送FIFO满之前写入数据,在接收FIFO非空时读取数据,避免溢出或欠载。

问题2:PWM输出频率或占空比不对。

  • 计算预分频器PR和匹配寄存器MR:重新核对F_pclkPRMR值的计算。一个常见的错误是忽略了PR+1因子。
  • 确认PWM周期寄存器:检查用于设置周期的匹配寄存器(如MR3)是否在MCR中配置了“复位TC”(MRnR=1)。
  • 检查引脚输出使能:确认MAT引脚是否已在IOCONFIG中正确配置为定时器匹配输出功能,而非普通的GPIO。
  • 使用示波器测量:直接观察波形是最直观的。测量周期和脉冲宽度,反推实际的计数值。

问题3:捕获功能测量的脉冲宽度值跳动很大。

  • 消抖处理:如果捕获的是机械开关等信号,需要在硬件(RC滤波)或软件(多次采样判断)上做消抖处理。
  • 处理计数器溢出:如果脉冲宽度可能超过TC的计数周期(对于16位定时器,当PR=0时,在50MHz下约1.3ms),必须在ISR中维护一个溢出计数器。计算宽度时:实际计数值 = 溢出次数 * (65536) + (t2 - t1)
  • 中断优先级与延迟:如果系统中断繁忙,捕获中断可能被延迟响应,造成误差。对于高精度测量,需要提高捕获中断的优先级,并确保ISR执行时间极短。

问题4:功耗异常。

  • 关闭未使用的外设时钟:在低功耗应用中,初始化完成后,检查SYSAHBCLKCTRL寄存器,确保只使能了真正需要的外设时钟。不用的SPI、定时器模块一定要关闭其时钟门控。
  • 配置未使用引脚:将未使用的GPIO引脚设置为确定的输出状态(高或低)或配置为带上拉的输入模式,避免浮空输入导致漏电流。

5. 进阶应用与性能考量

当你掌握了基础功能后,可以尝试一些更复杂的应用,这对系统的稳定性和性能提出了更高要求。

5.1 使用DMA配合SPI对于需要高速、大批量传输SPI数据的场景(如驱动TFT屏、读写大容量SPI Flash),频繁的CPU中断会成为瓶颈。EM773可能支持DMA(直接存储器访问),可以让DMA控制器自动将内存中的数据搬运到SPI发送FIFO,或从接收FIFO搬运到内存,无需CPU干预。这能极大解放CPU,并提高传输效率。你需要配置DMA的源地址、目标地址、传输数据量,并链接到SPI的外设请求。

5.2 定时器串联与同步单个16位定时器的最大定时长度有限。如果需要更长的定时,可以采用“预分频器+软件扩展”或“定时器串联”的方法。

  • 软件扩展:使能定时器的溢出中断,在中断服务程序中对一个全局变量(如timer_overflow_cnt)加1。这样,有效定时长度就从16位扩展到了16 + N位(N取决于全局变量的位数)。
  • 硬件串联:将一个定时器的匹配输出(MAT)连接到另一个定时器的捕获输入(CAP)。例如,用CT16B0产生一个固定的低频时钟(如1kHz方波),输出到MAT0引脚,并将该引脚连接到CT16B1(或CT32B0)的CAP0引脚。将CT16B1配置为在CAP0的上升沿计数。这样,CT16B1就变成了一个对CT16B0时钟进行计数的“二级定时器”,可以实现非常长的定时周期。

5.3 实时性保障在复杂的嵌入式系统中,多个外设中断可能同时发生。确保关键任务(如电机控制的PWM更新、通信协议的应答)的实时性至关重要。

  • 合理分配中断优先级:给SPI接收完成、定时器捕获等对时间敏感的中断分配更高的优先级。
  • 避免在中断中长时间阻塞:绝对禁止在ISR中使用delay()之类的忙等待函数。
  • 使用RTOS:对于多任务系统,考虑使用实时操作系统来管理任务调度和资源同步,比裸机前后台系统更能保证实时性。

最后,我想分享一个在调试EM773定时器PWM时遇到的具体问题。当时我需要生成一个非常低频的PWM(约0.5Hz),占空比需要精细调节。我直接使用了32位定时器CT32B0,将PR设置到最大,MR也设置了一个很大的值。但发现PWM输出极不稳定,偶尔会丢失脉冲。排查后发现,是因为用于设定周期的MR3值过于接近32位计数器的最大值,而用于设定占空比的MR0值也很大,在计算MR0MR3的比较时,由于代码中的一些非原子操作和中断干扰,导致了比较逻辑的瞬时错误。解决方案是:对于极低频PWM,不要追求单次定时器周期达到目标时长,而是利用一个周期较短(如10ms)的定时器中断,在中断中通过软件计数和操作GPIO来模拟低频PWM。硬件PWM更适合中高频场合。这个经历告诉我,数据手册给出的功能边界,有时需要结合具体应用场景和芯片实际性能来谨慎评估。