4. I2C/SPI通信协议实现:寄存器读写时序、DMA传输优化、中断处理机制、通信错误恢复策略
触摸控制器和SoC之间的通信,说白了就是两个芯片之间的「对话」。I2C和SPI是两种最常见的对话方式。我个人更偏爱SPI一些,因为速度快、时序简单。但I2C也有它的优势——引脚少,走线方便。
嗯,咱们今天就把这两种协议在触摸固件里的实现细节掰开揉碎讲清楚。我会结合自己踩过的坑,给你一些实战经验。
4.1 寄存器读写时序设计
触摸控制器内部有几十个寄存器,用来配置灵敏度、上报率、校准参数等。SoC通过I2C或SPI读写这些寄存器,才能控制触摸屏的工作状态。
4.1.1 I2C寄存器读写协议
I2C协议里,触摸控制器通常作为从设备。SoC先发设备地址(7位或10位),然后发寄存器地址,最后读写数据。
我习惯把寄存器地址设计成16位,这样地址空间够大,方便后续扩展。但要注意,有些低成本方案用8位地址,能省一点门电路。
标准I2C写时序(16位寄存器地址):
START + 设备地址(写) + ACK + 寄存器高8位 + ACK + 寄存器低8位 + ACK + 数据字节 + ACK + STOP
标准I2C读时序:
START + 设备地址(写) + ACK + 寄存器高8位 + ACK + 寄存器低8位 + ACK +
START + 设备地址(读) + ACK + 数据字节 + NACK + STOP
这里有个坑——读操作需要发两次START信号。我曾经在一个项目里忘了发第二次START,结果读回来的数据全是0xFF。排查了整整两天才发现问题。
4.1.2 SPI寄存器读写协议
SPI比I2C简单粗暴。通常用4线模式:SCK、MOSI、MISO、CS。CS拉低表示开始通信,拉高表示结束。
我设计的SPI协议是这样的:第一个字节是命令字,最高位表示读写方向(1=读,0=写),剩下7位是寄存器地址。后面跟数据字节。
// SPI写操作
CS拉低
发送命令字: 0x12 (写操作, 地址0x12)
发送数据: 0xA5
CS拉高
// SPI读操作
CS拉低
发送命令字: 0x92 (读操作, 地址0x12)
接收数据: 0xA5
CS拉高
你想想看,SPI的时序比I2C简单多了吧?但SPI有个缺点——需要4根线,占用的GPIO多。在引脚紧张的芯片上,I2C反而是更好的选择。
4.2 DMA传输优化
触摸屏上报数据时,一次可能传输几十个字节。如果用CPU逐字节搬运,会占用大量CPU时间。这时候DMA就派上用场了。
DMA说白了就是「硬件搬运工」。你告诉它数据从哪里来、到哪里去、搬多少,它自己就干完了,CPU可以腾出手来处理触摸算法。
4.2.1 SPI+DMA的配置要点
我常用的SPI+DMA配置是这样的:
// 以STM32为例
// 配置SPI为全双工主机模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
// 配置DMA通道
// TX通道:从内存到SPI数据寄存器
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)txBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
// RX通道:从SPI数据寄存器到内存
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
个人经验:DMA传输完成后一定要检查传输计数。我曾经遇到过DMA传输了50个字节,但实际只搬了48个的情况。原因是SPI的FIFO没清空,导致最后一个字节没发出去。
4.2.2 I2C+DMA的注意事项
I2C用DMA比SPI麻烦一些。因为I2C有ACK/NACK机制,DMA传输过程中如果从设备NACK了,DMA不会自动停止,会继续发数据。
我的做法是:I2C的DMA传输长度不要超过255字节。超过的话,分多次传输。每次传输完成后检查I2C状态寄存器,确认没有NACK错误。
4.3 中断处理机制
触摸控制器通常有两个中断引脚:INT和WAKE。INT用于上报触摸事件,WAKE用于唤醒系统。
中断处理是实时性的关键。处理不好,触摸就会卡顿、丢点。
4.3.1 中断优先级设计
我一般把触摸中断设为最高优先级。为什么?因为触摸是交互的核心,延迟超过10ms用户就能感觉到。
| 中断源 | 优先级 | 说明 |
|---|---|---|
| 触摸INT | 最高(0) | 实时响应触摸事件 |
| DMA传输完成 | 高(1) | 及时处理数据 |
| 定时器 | 中(2) | 扫描周期控制 |
| 其他外设 | 低(3) | 非关键任务 |
4.3.2 中断服务函数编写要点
中断服务函数要短小精悍。我有个原则:中断里只做三件事——读数据、存数据、清标志。数据处理放到主循环里做。
void INT_IRQHandler(void)
{
// 1. 读取触摸数据(通过SPI/I2C)
uint8_t data[64];
spi_read_burst(TOUCH_DATA_ADDR, data, 64);
// 2. 存入环形缓冲区
ring_buffer_push(&touch_buf, data, 64);
// 3. 清中断标志
touch_clear_int();
// 4. 通知主循环处理
touch_event_flag = 1;
}
注意:千万不要在中断里做浮点运算、malloc、printf等耗时操作。我曾经见过一个同事在中断里打印日志,结果触摸响应延迟了50ms,用户投诉说「触摸不跟手」。
4.4 通信错误恢复策略
通信不可能永远不出错。电磁干扰、电源波动、接触不良都可能导致通信失败。关键是要有健壮的错误恢复机制。
4.4.1 常见通信错误类型
- I2C总线锁死:SCL被拉低超过100ms,总线卡死
- SPI数据错位:CS时序异常,导致数据字节对齐错误
- ACK/NACK异常:从设备无响应,返回NACK
- 超时错误:通信超过预设时间未完成
4.4.2 我的恢复策略
我总结了一套「三步恢复法」:
- 重试:立即重试3次,每次间隔1ms。很多瞬态错误重试一次就好了。
- 复位通信外设:如果重试失败,复位SPI/I2C外设,重新初始化。
- 复位触摸芯片:如果还不行,拉低触摸芯片的复位引脚,强制重启。
// 通信错误恢复示例
int touch_comm_recover(void)
{
// 第一步:重试3次
for (int i = 0; i < 3; i++) {
if (touch_read_register(0x00) == SUCCESS) {
return SUCCESS; // 恢复成功
}
delay_ms(1);
}
// 第二步:复位通信外设
spi_deinit();
delay_ms(10);
spi_init();
// 第三步:复位触摸芯片
gpio_set_low(TOUCH_RESET_PIN);
delay_ms(5);
gpio_set_high(TOUCH_RESET_PIN);
delay_ms(20); // 等待芯片初始化完成
return touch_read_register(0x00) == SUCCESS ? SUCCESS : FAIL;
}
我曾经在一个量产项目里,发现约0.1%的设备在开机时通信失败。用了这套恢复策略后,失败率降到了0.001%以下。嗯,这就是经验的价值。
4.4.3 I2C总线锁死的特殊处理
I2C总线锁死是个很头疼的问题。SCL被从设备拉低,主设备无法产生时钟信号。
我的处理方法是:检测到SCL被拉低超过50ms,就主动产生9个时钟脉冲,让从设备释放总线。这招在90%的情况下都管用。
// I2C总线恢复:产生9个时钟脉冲
void i2c_bus_recover(void)
{
for (int i = 0; i < 9; i++) {
gpio_set_high(I2C_SCL_PIN);
delay_us(5);
gpio_set_low(I2C_SCL_PIN);
delay_us(5);
}
// 最后发一个STOP信号
gpio_set_low(I2C_SDA_PIN);
gpio_set_high(I2C_SCL_PIN);
delay_us(5);
gpio_set_high(I2C_SDA_PIN);
}
避坑指南:我曾经在一个项目里,I2C总线锁死后直接复位了触摸芯片。结果发现复位后总线还是锁死的——因为触摸芯片的I2C从机状态机没复位。后来改成先恢复总线,再复位芯片,问题就解决了。
4.5 本章知识体系
下面这张图展示了I2C/SPI通信协议实现的整体知识结构:
这四个部分环环相扣。寄存器读写是基础,DMA优化提升性能,中断处理保证实时性,错误恢复确保可靠性。少了任何一个,触摸固件都跑不长久。
好了,这一章的内容就到这里。记住一句话:通信协议实现得好不好,直接决定了触摸屏「好不好用」。多花点时间在通信健壮性上,绝对值得。
公众号:蓝海资料掘金营,微信deep3321