MSPM0 AES模块中断与轮询机制及GCM/CCM实战指南
1. MSPM0 AES模块:中断与轮询机制深度解析
在嵌入式系统开发,尤其是涉及数据安全的物联网设备中,如何高效、可靠地管理硬件加密引擎是每个开发者必须面对的课题。德州仪器(TI)的MSPM0 L系列微控制器集成了AESADV高级加密加速模块,它不仅支持标准的AES-ECB、CBC等基础模式,更提供了GCM和CCM这类同时兼顾机密性与完整性的认证加密模式。与这个强大的硬件引擎打交道,核心就在于理解其与CPU的“对话”机制——中断和轮询。
简单来说,中断就像你雇了一个秘书,加密引擎一完成工作就“敲门”通知你,你可以立刻去处理结果,期间CPU可以处理其他任务。而轮询则像你每隔几秒就探头去问秘书“工作做完了吗?”,直到得到肯定答复。MSPM0的AES模块设计得非常灵活,它提供了完整的事件发布机制,允许开发者根据系统实时性要求、中断延迟容忍度以及功耗约束,在两种方式间做出最合适的选择。
对于GCM/CCM这类涉及多阶段(AAD处理、加密/解密、TAG生成)的复杂操作,理解状态机的转换和相应的CPU通知机制尤为关键。模块通过三个独立的事件发布者(CPU_INT, DMA_TRIG_DATAIN, DMA_TRIG_DATAOUT)来管理这些通信,其中CPU_INT专门用于向CPU子系统报告加密操作的关键状态。本文将深入拆解这些机制,并结合GCM/CCM的实际操作流程,为你呈现一套从原理到实践的完整指南。
2. AES模块事件系统与寄存器精讲
要驾驭MSPM0的AES模块,首先必须吃透其事件和中断系统。这不仅仅是配置几个寄存器那么简单,而是理解整个数据流和控制流如何协同工作的基础。
2.1 核心事件发布者与功能划分
AESADV模块设计了三个独立的事件发布者,各司其职,结构清晰:
- CPU_INT: 这是与CPU交互的主通道。它管理着AES模块发给CPU的中断请求(IRQ),通过静态事件路由连接到CPU子系统。当加密操作完成、有新数据可读/写、或上下文就绪时,都通过它来通知CPU。
- DMA_TRIG_DATAIN: 这是数据输入的“油门”。当AES引擎的输入缓冲区为空,准备好接收下一个数据块时,它会发布DMA触发事件0。这通常用于配置DMA通道,将源数据(如SRAM中的明文或AAD)自动搬运到AES的DATA_IN寄存器。
- DMA_TRIG_DATAOUT: 这是数据输出的“哨兵”。当AES引擎完成一个数据块的计算,输出缓冲区有数据可读时,它会发布DMA触发事件1。这用于触发DMA将结果(密文或解密后的明文)从DATA_OUT寄存器搬移到目标内存。
这种分离的设计非常巧妙。在典型的DMA配合场景下,DMA_TRIG_DATAIN和DMA_TRIG_DATAOUT这两个事件会与DMA控制器联动,实现数据搬运的自动化。而CPU_INT则用于处理更高级别的状态通知,例如整个操作的完成、或上下文的保存就绪,让CPU从频繁的数据搬运中断中解放出来,只需在关键节点进行干预。
2.2 CPU中断事件详解与轮询的基石:RIS寄存器
CPU_INT事件发布者管理着四个核心的中断源,其状态完全由一组寄存器控制,这是我们实现轮询机制的关键所在。
- IIDX (Interrupt Index Register, 偏移 0x1020): 中断索引寄存器。这是一个只读寄存器,硬件会自动将其更新为当前已使能(通过IMASK)且优先级最高的待处理中断的索引号。读取这个寄存器有一个重要的副作用:硬件会自动清除该中断在RIS和MIS中的标志位,并更新为下一个最高优先级的中断索引。如果所有已使能中断都已处理,则读回0。注意:这个“自动清除”特性意味着,如果你在中断服务程序(ISR)中读取IIDX来判断中断源,它就已经帮你清理了现场;但如果你采用轮询方式,直接读RIS寄存器是更安全的选择,避免状态被意外清除。
- IMASK (Interrupt Mask Register, 偏移 0x1028): 中断掩码寄存器。可读写,用于使能或禁用特定的中断源。某位置1表示对应中断未被屏蔽(即允许产生中断)。只有未被屏蔽的中断,其状态才会反映到IIDX和MIS寄存器中。
- RIS (Raw Interrupt Status Register, 偏移 0x1030):原始中断状态寄存器。这是轮询机制的核心。它是一个只读寄存器,无论IMASK如何设置,任何中断条件发生,对应的位都会被置1。这意味着即使你关闭了所有中断(IMASK全0),仍然可以通过循环读取RIS寄存器来检测AES模块的状态。这正是实现纯轮询操作的硬件基础。
- MIS (Masked Interrupt Status Register, 偏移 0x1038): 屏蔽后中断状态寄存器。只读,其值是IMASK和RIS的按位与结果。它反映的是当前已使能的中断源状态。
- ISET (Interrupt Set Register, 偏移 0x1040): 中断设置寄存器。只写,向某位写1可以软件模拟该中断事件的发生,同时置位对应的RIS位。这在诊断、测试或特定软件流程中非常有用。
- ICLR (Interrupt Clear Register, 偏移 0x1048): 中断清除寄存器。只写,向某位写1可以清除对应的RIS标志位,无论该中断是否被IMASK屏蔽。这给了软件最大的灵活性来管理中断状态。
CPU_INT管理的四个中断条件及其在RIS/IMASK中的位定义如下:
| 位 | 名称 (IIDX值) | 描述 |
|---|---|---|
| 0 | OUTPUTRDY (1) | 输出就绪。表示AES引擎有一个128位的输出块(例如,加密/解密后的数据,或GHASH的中间结果)可供CPU读取。注意:如果启用了DMA握手(DMA_HS.DMA_DATA_ACK=1),则不应使用此中断,数据搬运应由DMA自动完成。 |
| 1 | INPUTRDY (2) | 输入就绪。表示AES引擎的输入缓冲区为空,可以接收下一个128位的数据块。同样,在DMA握手启用时不应使用。 |
| 2 | SAVEDCNTXTRDY (3) | 保存的上下文就绪。这是一个极其重要的状态位。当CTRL.SAVE_CNTXT位被置1,且一次认证加密操作(如GCM, CCM, CBC-MAC)完成或通过GET_DIGEST命令请求了一个中间摘要时,此位被置1。它表示认证标签(TAG)和/或IV块已计算完毕,并保存在TAG0-3和/或IV0-3寄存器中,等待CPU读取。此位与CNTXTRDY互斥。 |
| 3 | CNTXTRDY (4) | 上下文就绪。表示上下文数据寄存器(KEY, IV, CTRL, 长度寄存器等)可以被覆盖,CPU可以写入新的上下文(即启动一次新的加密操作)。 |
轮询操作的精髓: 文档中特别提到的“Poll the RIS register continuously for SAVEDCNTXTRDY status instead of waiting for interrupt”,其实现就是基于RIS寄存器的特性。你可以在一个循环中不断读取RIS寄存器的值,并检查其第2位(SAVEDCNTXTRDY)是否为1。由于RIS不受IMASK影响,即使你关闭了中断,也能可靠地检测到这一状态。当检测到该位置位后,你可以安全地去读取TAG0-3寄存器获取认证标签,然后通过写ICLR寄存器的第2位来清除此状态标志。
注意: 在轮询
SAVEDCNTXTRDY时,务必确保CTRL.SAVE_CNTXT位已被正确设置为1,否则该状态永远不会被置位。同时,读取TAG寄存器这个动作本身,在某些情况下也可能帮助硬件清除相关状态,但最规范的做法是显式地写ICLR寄存器进行清除。
2.3 DMA触发事件与握手配置
DMA_TRIG_DATAIN和DMA_TRIG_DATAOUT这两组寄存器的结构与CPU_INT类似,也包含IIDX、IMASK、RIS、MIS、ISET、ICLR,但它们各自只管理一个事件:TRIG0和TRIG1。
要使用DMA进行数据自动搬运,必须进行以下配置:
- 在AES模块中,设置对应DMA触发事件的IMASK位(例如,为
DMA_TRIG_DATAIN启用TRIG0)。 - 在DMA控制器中,将通道的触发源选择为AES对应的Trig0或Trig1。
- 关键一步: 将
DMA_HS.DMA_DATA_ACK位设置为1。这个操作将AES模块的数据应答机制从“寄存器访问模式”切换为“DMA握手模式”。在此模式下,INPUTRDY和OUTPUTRDY中断将不再产生(文档明确提示不应使用),数据块的传输就绪信号将通过DMA触发事件来传递,从而实现与DMA控制器的硬连线协作,极大提升数据传输效率。
3. GCM与CCM操作原理及模式详解
GCM和CCM是两种广泛使用的认证加密模式,它们在提供保密性(加密)的同时,还提供完整性和真实性认证(生成TAG)。MSPM0的AESADV模块对这两种模式提供了硬件级的强力支持。
3.1 GCM操作模式深度剖析
GCM = Galois/Counter Mode。它本质上结合了CTR模式(用于加密)和Galois域乘法(用于认证)。其核心优势是并行化和高性能。
GCM的关键组件与初始化:
- H: 认证密钥。由加密密钥通过一次AES-ECB加密一个全零块得到。即
H = AES-Encrypt(Key, 0)。 - J0: 初始计数器块。由IV(通常12字节)衍生而来。在MSPM0中,当IV非96位时,需要通过GHASH计算
J0 = GHASH(H, IV || 0...0)。 - Y0: 加密后的J0,即
Y0 = AES-Encrypt(Key, J0)。这是CTR模式加密的起始计数器值。
MSPM0的AES模块通过CTRL.GCM[1:0]位域提供了三种GCM子模式,以适应不同场景:
01b(GCM=1):GHASH only, H预加载,Y0强制为零。此模式仅执行认证(GHASH),不执行加密/解密。你需要预先计算好H并写入GHASH_H0-3寄存器,且引擎内部将Y0视为零。适用于仅需生成或验证GMAC(无载荷的GCM)的场景。10b(GCM=2):GCM with pre-calculated H。这是最常用的模式之一。你提供密钥、预计算的H、以及IV(用于内部计算J0和Y0)。引擎会自行计算Y0并执行完整的GCM(加密+认证)。这平衡了性能与灵活性。11b(GCM=3):Autonomous GHASH。完全自主模式。你只需提供密钥和IV,引擎内部自动计算H和Y0,并执行完整的GCM操作。这是最方便的模式,但可能在某些需要重复使用H的场合略有性能损失。
GCM数据填充与内存布局的重要约束:文档中特别强调了一个易错点:AAD(附加认证数据)和加密数据(明文/密文)的字节流不需要在各自的末尾填充到128位边界,但整个组合流(AAD在前,加密数据在后)在内存中的布局必须确保加密数据从一个128位(16字节)对齐的地址开始。
- CPU(中断)模式: 由于CPU可以分别、按块提供AAD和加密数据,所以没有连续内存布局的限制。你只需要分别写入AAD块和加密数据块即可。
- DMA模式: 因为通常使用单个DMA通道来连续输送数据,所以AAD和加密数据必须在内存中连续存放,且加密数据的起始地址必须是16字节对齐的。如果AAD的长度不是16字节的整数倍,CPU必须在AAD的末尾填充零字节(0x00),使得加密数据的起始地址对齐。填充的字节数
n满足0 <= n <= 15(因为填充是以字节为单位)。例如,AAD长度为30字节,则需要填充2个零字节,使总长度变为32字节(2个块),这样接下来的加密数据自然从新的块边界开始。
3.2 CCM操作模式深度剖析
CCM = Counter with CBC-MAC。它结合了CBC-MAC(用于认证)和CTR模式(用于加密)。这是一个串行模式,认证和加密操作依次进行。
CCM协议流程简述:
- 认证阶段(CBC-MAC):
- 首先构造认证块B0(包含标志、Nonce、消息长度)。
- 接着处理AAD(如果需要):构造B1(包含AAD长度),然后跟上AAD数据。如果AAD长度不为零,且不是块对齐的,同样需要填充零。
- 然后处理明文数据:将每个明文块作为CBC-MAC的输入。CBC-MAC的初始向量(IV)是零。
- 加密阶段(CTR):
- 使用CTR模式加密明文,生成密文。CTR的计数器从A0(由Nonce和计数器0构成)开始递增。
- 最终认证标签生成:
- 将CBC-MAC阶段得到的最终认证结果(一个128位值)用CTR模式下的一个特定计数器(与加密所用计数器序列不同)进行加密,得到最终的TAG。
MSPM0中CCM的关键配置参数:
CTRL.CCML: 定义长度字段“L”的宽度(字节数)。L = CCML + 1。它决定了Nonce的长度(15-L)。L的有效范围是2到8(即CCML=1到7),这直接影响IV的构造。CTRL.CCMM: 定义认证字段“M”的长度(字节数)。M = 2 * (CCMM + 1)。最终生成的TAG是128位,但只有最低的M字节是有效的。你需要根据通信协议的要求来设置此值(例如,IEEE 802.15.4常用M=4,即8字节TAG中取4字节)。CTRL.CTR_WIDTH: 计数器宽度。必须设置得足够大,以确保在加密整个消息的过程中计数器不会回绕溢出。对于CCM,CTR_WIDTH必须至少能覆盖CCML定义的计数器字段。
4. 实战指南:基于轮询的GCM/CCM操作流程
理解了原理和寄存器,我们来看如何将它们组合起来,完成一次完整的、基于轮询(而非中断)的GCM或CCM操作。这里以GCM加密(使用预计算H)为例,CCM流程类似,主要区别在上下文配置和寄存器设置。
4.1 系统初始化与模块使能
在操作任何外设前,基础的系统初始化必不可少。
// 1. 使能AES模块的时钟(具体寄存器取决于MSPM0系列,此处为示例) SYSCTL->CLK_EN |= SYSCTL_CLK_EN_AES_MASK; // 2. 解除AES模块复位(如果之前被复位) AES->RSTCTL = 0xB1; // 写入KEY以解锁 AES->RSTCTL |= 0x1; // 置位RESETASSERT,然后清除以解除复位(具体操作需查手册) // 通常,解除复位是清除RESETASSERT位,假设写0解除 AES->RSTCTL = 0xB1; // 重新写入KEY AES->RSTCTL &= ~0x1; // 清除RESETASSERT位 // 3. 检查复位状态(可选) while(AES->STAT & (1<<16)) { /* 等待复位粘滞位清除 */ } // 4. 使能AES模块电源(如果需要) AES->PWREN = 0x26; // 写入KEY AES->PWREN |= 0x1; // 置位ENABLE4.2 GCM加密(预计算H模式)轮询实现
假设我们已经预先计算好了H(H = AES-Encrypt(Key, 0)),并存储在变量precomputed_H[4]中。我们要加密一段数据,并包含AAD。
// 宏定义和变量声明 #define AES_BLOCK_SIZE 16 // 字节 uint32_t *plaintext; // 指向明文数据的指针 uint32_t *ciphertext; // 指向密文存储区的指针 uint32_t *aad_data; // 指向AAD数据的指针 uint32_t plaintext_len_bytes; // 明文长度(字节) uint32_t aad_len_bytes; // AAD长度(字节) uint32_t key[8]; // 256位密钥,如果是128位则只用前4个 uint32_t iv[4]; // 128位IV // --- 步骤 1: 配置AES模块为轮询模式,禁用中断 --- // 清除所有CPU中断掩码,我们完全依赖轮询RIS寄存器 AES->IMASK = 0x00000000; // 同样,如果我们不用DMA,也禁用DMA触发事件的中断掩码 AES->DMA_TRIG_DATAIN.IMASK = 0x0; AES->DMA_TRIG_DATAOUT.IMASK = 0x0; // 确保DMA握手模式关闭,因为我们用CPU轮询方式读写数据 AES->DMA_HS &= ~(1<<0); // 清除DMA_DATA_ACK位,使用寄存器I/O模式 // --- 步骤 2: 加���密钥 --- // 写入KEY0-KEY7寄存器。注意:必须先确保CTRL.KEYWR状态为0(可写) while(AES->STATUS & 0x1) { /* 等待KEYWR状态为0,如果为1可能需要复位模块 */ } AES->KEY0 = key[0]; AES->KEY1 = key[1]; AES->KEY2 = key[2]; AES->KEY3 = key[3]; // 如果是256位密钥,继续写入KEY4-KEY7 AES->KEY4 = key[4]; AES->KEY5 = key[5]; AES->KEY6 = key[6]; AES->KEY7 = key[7]; // --- 步骤 3: 加载初始化向量IV --- AES->IV0 = iv[0]; AES->IV1 = iv[1]; AES->IV2 = iv[2]; AES->IV3 = iv[3]; // --- 步骤 4: 加载预计算的H到GHASH寄存器 --- AES->GHASH_H0 = precomputed_H[0]; AES->GHASH_H1 = precomputed_H[1]; AES->GHASH_H2 = precomputed_H[2]; AES->GHASH_H3 = precomputed_H[3]; // --- 步骤 5: 配置控制寄存器CTRL --- uint32_t ctrl_value = 0; ctrl_value |= (3 << 3); // KEY_SIZ[1:0] = 3, 选择256位密钥。128位密钥则设为1。 ctrl_value |= (1 << 2); // DIR = 1, 加密操作 ctrl_value |= (2 << 16); // GCM[1:0] = 2 (10b), 使用预计算H的GCM模式 ctrl_value |= (1 << 6); // CTR = 1, 必须置位以启用CTR加密(GCM的一部分) ctrl_value |= (1 << 29); // SAVE_CNTXT = 1, 非常重要!操作完成后保存TAG和上下文。 // 注意:CTRL[31] CNTXT_RDY 和 [30] SAVED_CNTXT_RDY 是只读状态位,不能写入。 // 其他位如CBC, CFB, ICM等保持为0。 AES->CTRL = ctrl_value; // --- 步骤 6: 写入数据长度寄存器 --- // 首先写入AAD长度(字节数)。注意:长度寄存器触发上下文加载! AES->AAD_LENGTH = aad_len_bytes; // 接着写入加密数据长度(字节数)。写入C_LENGTH会启动上下文加载和数据处理。 // 长度必须是字节数。对于非块对齐的数据,引擎内部会处理,但总字节数需准确。 AES->C_LENGTH_0 = plaintext_len_bytes & 0xFFFFFFFF; AES->C_LENGTH_1 = (plaintext_len_bytes >> 32) & 0x1FFFFFFF; // 高29位有效 // --- 步骤 7: 提供AAD数据(轮询INPUTRDY状态)--- uint32_t aad_blocks = (aad_len_bytes + 15) / 16; // 计算AAD的完整块数(包括填充) for(uint32_t i = 0; i < aad_blocks; i++) { // 等待输入缓冲区就绪:轮询RIS寄存器的INPUTRDY位(位1) while((AES->RIS & (1<<1)) == 0) { // 空循环,等待。可以在这里加入超时机制。 } // 写入一个128位(4个字)的数据块 AES->DATA0 = aad_data[i*4]; AES->DATA1 = aad_data[i*4 + 1]; AES->DATA2 = aad_data[i*4 + 2]; AES->DATA3 = aad_data[i*4 + 3]; // 注意:如果AAD长度不是16字节的整数倍,最后一个块需要软件填充0。 // 例如,aad_len_bytes=30,则最后一个块的后2个字节(即最后1个字的低16位)应为0。 } // --- 步骤 8: 提供加密数据并读取结果(轮询OUTPUTRDY状态)--- uint32_t crypto_blocks = (plaintext_len_bytes + 15) / 16; uint32_t words_written = 0; uint32_t words_read = 0; for(uint32_t i = 0; i < crypto_blocks; i++) { // 1. 等待输入就绪,然后写入一个明文块 while((AES->RIS & (1<<1)) == 0); // 轮询INPUTRDY AES->DATA0 = plaintext[words_written++]; AES->DATA1 = plaintext[words_written++]; AES->DATA2 = plaintext[words_written++]; AES->DATA3 = plaintext[words_written++]; // 2. 对于非最后一个块,写入后需要等待输出就绪,然后读取上一个块的结果(流水线操作) // 注意:AES引擎是流水线的,在写入第N个块后,可以读取第N-1个块的结果。 if(i > 0) { // 从第二个输入块开始,可以读取第一个输出块 while((AES->RIS & (1<<0)) == 0); // 轮询OUTPUTRDY ciphertext[words_read++] = AES->DATA0; ciphertext[words_read++] = AES->DATA1; ciphertext[words_read++] = AES->DATA2; ciphertext[words_read++] = AES->DATA3; } } // 3. 读取最后一个输出块 while((AES->RIS & (1<<0)) == 0); // 轮询OUTPUTRDY ciphertext[words_read++] = AES->DATA0; ciphertext[words_read++] = AES->DATA1; ciphertext[words_read++] = AES->DATA2; ciphertext[words_read++] = AES->DATA3; // --- 步骤 9: 轮询SAVEDCNTXTRDY状态并读取认证标签TAG --- // 这是轮询替代中断的核心步骤。我们不断检查RIS[2]。 while((AES->RIS & (1<<2)) == 0) { // 等待SAVEDCNTXTRDY标志置位。这表示GCM操作已完成,TAG已就绪。 } // 标志置位,读取TAG uint32_t tag[4]; tag[0] = AES->TAG0; tag[1] = AES->TAG1; tag[2] = AES->TAG2; tag[3] = AES->TAG3; // --- 步骤 10: 清除SAVEDCNTXTRDY状态标志 --- // 通过写ICLR寄存器的对应位来清除RIS标志。 AES->ICLR = (1 << 2); // 写1清除第2位(SAVEDCNTXTRDY) // 至此,一次完整的GCM加密(轮询方式)完成。 // 密文存储在ciphertext,认证标签存储在tag中。4.3 CCM加密操作配置要点
CCM的流程与GCM类似,但配置和上下文加载有区别。以下是关键步骤的差异:
// 假设已进行模块使能、密钥加载等初始化步骤... // 1. 加载IV。对于CCM,IV寄存器需要包含特定的格式:标志(Flags) + Nonce。 // 构造IV需要根据CCM规范,将L、M等信息编码进去。 // 例如,对于一个L=2(即Nonce长度13字节),M=4(8字节TAG取4字节)的配置: // IV[0-3]需要包含1字节Flags和13字节Nonce,剩余2字节填充(可能为0或消息长度)。 // 具体构造方法请参考NIST SP 800-38C或RFC 3610。 construct_ccm_iv(iv_array, nonce, msg_len, aad_len, L, M); AES->IV0 = iv_array[0]; AES->IV1 = iv_array[1]; AES->IV2 = iv_array[2]; AES->IV3 = iv_array[3]; // 2. 配置CTRL寄存器 uint32_t ctrl_value = 0; ctrl_value |= (1 << 3); // KEY_SIZ, 例如128位密钥 ctrl_value |= (1 << 2); // DIR = 1, 加密 ctrl_value |= (1 << 18); // CCM = 1, 启用CCM模式 ctrl_value |= (1 << 6); // CTR = 1, 必须置位 ctrl_value |= (1 << 29); // SAVE_CNTXT = 1 ctrl_value |= ((M_value) << 22); // CCMM, 例如M=4 -> CCMM=1 (因为 M=2*(CCMM+1)) ctrl_value |= ((L_value) << 19); // CCML, 例如L=2 -> CCML=1 (因为 L=CCML+1) ctrl_value |= (desired_ctr_width << 7); // CTR_WIDTH, 根据消息长度选择,确保计数器不溢出 AES->CTRL = ctrl_value; // 3. 写入长度寄存器。注意顺序:先AAD_LENGTH,后C_LENGTH。 AES->AAD_LENGTH = aad_len_bytes; AES->C_LENGTH_0 = plaintext_len_bytes & 0xFFFFFFFF; AES->C_LENGTH_1 = (plaintext_len_bytes >> 32) & 0x1FFFFFFF; // 4. 后续的数据提供(AAD -> Crypto Data)和状态轮询(SAVEDCNTXTRDY)流程与GCM示例相同。 // 5. 读取的TAG是128位,但只有低 (M*2) 字节有效,需要根据协议截取。5. 关键注意事项与排错指南
在实际开发中,仅仅按照流程操作是不够的,理解那些容易导致失败的细节和陷阱至关重要。
5.1 常见配置陷阱与解决方案
长度寄存器写入导致意外启动:
- 问题: 写入
C_LENGTH或AAD_LENGTH寄存器会触发AES引擎开始使用当前已加载的上下文(密钥、IV、模式等)进行处理。如果你在配置完CTRL后,先写入了数据,再写长度寄存器,引擎可能不会按预期工作。 - 解决:严格遵守顺序:加载密钥(KEY) -> 加载IV -> 加载其他上下文(如GHASH_H)-> 配置CTRL -> 写入AAD_LENGTH -> 写入C_LENGTH。写入C_LENGTH是“发令枪”。
- 问题: 写入
SAVEDCNTXTRDY 永不置位:
- 检查1:
CTRL.SAVE_CNTXT位是否设置为1?如果为0,引擎完成操作后不会保存上下文,自然不会置位该标志。 - 检查2: 操作模式是否正确?只有生成认证标签的模式(GCM, CCM, CBC-MAC)在完成后才会设置此标志。单纯的ECB、CBC加密不会。
- 检查3: 数据流是否已真正结束?确保你已提供了
AAD_LENGTH和C_LENGTH指定的所有字节的数据(包括填充)。引擎在处理完所有数据后才会进入“完成”状态。
- 检查1:
DMA与CPU模式混用导致错误:
- 问题: 如果设置了
DMA_HS.DMA_DATA_ACK=1(启用DMA握手),却尝试用CPU轮询INPUTRDY/OUTPUTRDY来读写数据,会导致握手信号混乱,数据可能无法正确传输。 - 解决: 明确选择一种数据传递方式。纯CPU轮询/中断模式:设置
DMA_DATA_ACK=0,使用DATA0-3寄存器,并轮询RIS的0、1位。DMA模式:设��DMA_DATA_ACK=1,配置好DMA通道,并启用对应的DMA_TRIG事件掩码。
- 问题: 如果设置了
GCM/CCM数据对齐与填充错误:
- 问题: 在DMA模式下,AAD和加密数据在内存中不连续,或加密数据起始地址未16字节对齐,导致认证计算错误,TAG验证失败。
- 解决: 在准备DMA源数据缓冲区时,确保AAD和加密数据是连续的。计算AAD的填充长度:
padding_len = (16 - (aad_len % 16)) % 16。在AAD数据后添加padding_len个0x00字节。确保加密数据的指针(aad_data + aad_len_with_padding)是16字节对齐的。
5.2 调试技巧与状态检查
- 利用STATUS寄存器: 在写入密钥前,检查
AES->STATUS的KEYWR位。如果为1,表示密钥寄存器被锁定,写入无效。此时需要复位AES模块(通过RSTCTL寄存器)。 - 监控CTRL状态位:
CTRL.CNTXT_RDY(位31)指示是否可以加载新上下文。CTRL.SAVED_CNTXT_RDY(位30)是SAVEDCNTXTRDY中断的标志位在寄存器中的直接映射。在轮询RIS的同时,也可以直接读CTRL观察这些位。 - 超时机制: 在轮询循环中(如等待
INPUTRDY、OUTPUTRDY、SAVEDCNTXTRDY),一定要加入超时计数器。避免因硬件故障或配置错误导致软件死循环。#define POLL_TIMEOUT 1000000 uint32_t timeout = 0; while((AES->RIS & (1<<2)) == 0) { timeout++; if(timeout > POLL_TIMEOUT) { // 处理超时错误:检查配置、数据长度,或复位AES模块 handle_aes_timeout_error(); break; } } - 上下文保存与恢复(高级操作): 对于GCM/CCM,如果处理一个超长的数据流需要中断,可以使用
CTRL.GET_DIGEST位来请求一个中间摘要。当SAVEDCNTXTRDY置位时,除了读取TAG,还可以读取BLK_CNT0/1寄存器来保存当前的块计数器。恢复时,写入保存的中间TAG到GCMCCM_TAG0-3,写入块计数器到BLK_CNT0/1,然后设置CTRL.GCM_CONT(对于GCM)或CTRL.OFB_GCM_CCM_CONT(对于CCM的AAD阶段)和CTRL.GCM_CONT(对于CCM的加密阶段),并重新写入长度寄存器,即可从断点继续。
5.3 性能优化考量
- 轮询 vs 中断: 轮询消耗CPU周期,但响应延迟确定且极低。中断响应有延迟,但允许CPU在等待时执行其他任务。对于单次、短数据的加密,轮询可能更简单高效。对于长时间、流式的加密,或者系统有其他高优先级任务时,使用DMA+中断可能是更好的选择。
- DMA的使用: 对于大量数据的加密,务必使用DMA。配置两个DMA通道,分别处理
DATA_IN和DATA_OUT的触发。这能将CPU从数据搬运中彻底解放出来,CPU只需在开始时配置上下文,在结束时(通过SAVEDCNTXTRDY中断或轮询)读取TAG。 - 密钥与H的预计算: 如果多次操作使用相同密钥,在GCM模式下预计算H并保存,可以节省每次初始化时的一次AES-ECB运算时间。
通过深入理解MSPM0 AES模块的中断/轮询机制,并熟练掌握GCM/CCM的配置流程和避坑要点,你就能在资源受限的嵌入式环境中,稳健地实现高性能的硬件加密功能,为你的物联网设备、通信模块或其他安全敏感应用筑牢基础。记住,安全无小事,每一个配置位的准确理解,都是产品可靠性的基石。
