背景
最近参加公司一个电池测试仪的项目,负责电容屏驱动开发,电容屏的触控IC是汇顶科技的GT911
,电容屏的总线接口是I2C
。
因为项目沟通方面的失误,本应接到主控芯片的电容屏,被连到了MSP430
这款负责供电管理的MCU,领导说等PCB改版太慢了,让我就在MSP430上开发驱动,于是电容屏接入系统的方式就变成了这样:
本来写一份驱动就行,现在得MSP430和主机各写一部分,而且两边还要加一些SPI
总线处理。
MSP430的I2C驱动调试
首先要打通MSP430跟电容屏之间的I2C通信。
I2C控制器的初始化
分配I2C控制器对应的GPIO管脚
需要配置PxSEL0和PxSEL1寄存器,二者的相同位置的一对bit可以确定一个GPIO管脚是复用成4种功能的哪一种,我们根据datasheet选择合适的值,就能将其配置成I2C的SCL,同理SDA要用另外一对bit
配置I2C工作模式和波特率
这需要I2C控制器在复位状态下才能进行,因此需要先设置UCBxCTLW0的UCSWRST位,配置完毕后再清零相应bit位。
工作模式需要先设置UCBxCTLW0的UCMODE_3位,表示该USCI_B模块工作在I2C模式下,再设置UCMST位,表示I2C扮演Master角色。
波特率需要先设置UCBxCTLW0的UCSSEL__SMCLK位,表示时钟源选择SMCLK,再将UCB1BRW寄存器的值配成20,表示将SMCLK(我的是8M)分频20倍,就得到了400K,是GT911能支持的最高时钟频率。
配置I2C接收完成、发送完成、收发NACK中断
因为承载I2C功能的是USCI_B模块,因此中断向量要这样写
#pragma vector = USCI_B1_VECTOR
__interrupt void USCI_B1_ISR(void)
{
switch(__even_in_range(UCB1IV, USCI_I2C_UCBIT9IFG))
{
case USCI_NONE: break; // Vector 0: No interrupts
case USCI_I2C_UCALIFG: break; // Vector 2: ALIFG
case USCI_I2C_UCNACKIFG: // Vector 4: NACKIFG
// NACK中断的处理
UCB1CTLW0 |= UCTXSTP; // pull up SCL
break;
case USCI_I2C_UCSTTIFG: break; // Vector 6: STTIFG
case USCI_I2C_UCSTPIFG: break; // Vector 8: STPIFG
case USCI_I2C_UCRXIFG3: break; // Vector 10: RXIFG3
case USCI_I2C_UCTXIFG3: break; // Vector 12: TXIFG3
case USCI_I2C_UCRXIFG2: break; // Vector 14: RXIFG2
case USCI_I2C_UCTXIFG2: break; // Vector 16: TXIFG2
case USCI_I2C_UCRXIFG1: break; // Vector 18: RXIFG1
case USCI_I2C_UCTXIFG1: break; // Vector 20: TXIFG1
case USCI_I2C_UCRXIFG0: // Vector 22: RXIFG0
// 接收完成中断的处理
break;
case USCI_I2C_UCTXIFG0: // Vector 24: TXIFG0
// 发送完成中断的处理
break;
case USCI_I2C_UCBCNTIFG: break; // Vector 26: BCNTIFG
case USCI_I2C_UCCLTOIFG: break; // Vector 28: clock low timeout
case USCI_I2C_UCBIT9IFG: break; // Vector 30: 9th bit
default: break;
}
}
注意,NACK中断一定要使能,且在中断里发送stop condition
,这会让SCL信号重新拉高,不这样做的话,一旦I2C地址输错,就会出现很诡异的现象:SCL一直为低。
其他两个中断在下面讲。
I2C控制器的发送、接收流程
I2C发送流程
主循环的处理流程
- 记录tx_buf、tx_buf_len、tx_buf_offset到全局变量
- 将slave的I2C地址写入UCBxI2CSA寄存器
- 设置UCBxCTLW0的UCTR位,表示自己扮演Transmitter,同时设置UCTXSTT位,在线路上生成
start condition
- 等待tx_buf_offset == tx_buf_len
发送完成中断的处理流程
第一次I2C发送完成中断貌似是由I2C控制器发送完slave地址后触发的,后面的中断都是buf内第tx_buf_offset个字节发送完毕触发,不过中断里面的流程是一样的:
- tx_buf_offset是否等于tx_buf_len
- 是,说明buf发送完毕,设置UCBxCTLW0的UCTXSTP,发送
stop condition
,清零UCBxIFG寄存器的UCTXIFG位,主循环稍后会退出 - 否,说明buf还未发完,将第tx_buf_offset个字节送入UCBxTXBUF寄存器,该寄存器的内容会被发送到线路,然后tx_buf_offset++
I2C接收流程
- 记录rx_buf、rx_buf_len、rx_buf_offset到全局变量
- 将slave的I2C地址写入UCBxI2CSA寄存器
- 清零UCBxCTLW0的UCTR位,表示自己扮演Receiver,同时设置UCTXSTT位,在线路上生成
start condition
- 等待rx_buf_offset == rx_buf_len
接收完成中断的处理流程
I2C控制器扮演receiver角色时,发送完slave地址貌似不会进入发送完成中断,当然也不会进入接收完成中断,所以就简单一些:
- rx_buf_offset是否等于rx_buf_len
- 是,说明buf接收完毕,设置UCBxCTLW0的UCTXSTP,发送
stop condition
,将UCBxRXBUF的内容存入rx_buf[rx_buf_offset],主循环稍后会退出 - 否,说明buf还未收完,将将UCBxRXBUF的内容存入rx_buf[rx_buf_offset],然后rx_buf_offset++
电容屏的MSP430驱动调试
电容屏的初始化
分配I2C之外的RST和INT管脚
I2C只是数据传输通道,但电容屏还需要RST(复位)管脚来获悉什么时候复位其内部状态,还需要INT(中断)管脚来主动告知MSP430触摸事件的发生,快来通过I2C读取触摸坐标吧
RST是输出管脚,且GT911要求默认为高电平,因此要先设置PxDIR的特定bit表示输出,再设置PxOUT的相应bit表示输出高电平。
INT按理说是输入管脚,但GT911还用它来配置slave地址,因此要先配置成输出,在复位一段约定的时间后,输出一个高/低的电平,让GT911知道自己的I2C地址是0x14还是0x5D,地址配好之后,再将该管脚配置成输入。
电容屏的复位流程
- 执行上面的管脚分配和初始化流程
- 延迟5ms后,拉低RST管脚,复位GT911
- 延迟20ms后,让INT管脚输出低电平,GT911从而知道自己的I2C地址是0x5D
- 延迟1ms后,拉高RST管脚
- 延迟10ms后,将RST管脚配置成输入,确保其不会无意中复位GT911
- 让INT管脚输出低电平
- 延迟50ms后,将INT管脚配置成输入
电容屏的初始化流程
电容屏的固件初始化
- 延迟5ms后,读取0x8140(CMD)寄存器获取GT911的chip PID
- 向CMD寄存器写入0x02复位GT911的固件
- 从0x8047(CFG_DATA)寄存器读取GT911的固件版本号
- 计算GT911的配置参数的校验和
- 将配置参数写到CFG_DATA起始的寄存器空间
- 将校验和写入0x80FF(CFG_CHECKSUM)寄存器
- 延迟1ms后,向CMD寄存器写入0x0,使得固件退出复位状态,进入坐标读取状态
- 向0x814E(COOR_ADDR)寄存器写0,清除上报的坐标信息
电容屏的中断初始化
GT911默认是下降沿触发,且是浮动输入
- 设置PxIES的相应bit,表示下降沿触发
- 清零PxREN的相应bit,表示浮动输入,不使能上拉or下拉电阻
- 设置PxIE相应bit,使能INT管脚的中断
电容屏的坐标读取流程
中断响应
为了最小化中断耗时,只在中断里更新一个触摸事件计数器,并清零中断,其他都在主循环里完成
#pragma vector = PORT2_VECTOR
__interrupt void GPIO_PORT2_ISR(void)
{
switch(__even_in_range(P2IV, P2IV_P2IFG7))
{
case P2IV_NONE: break;
case P2IV_P2IFG0: break;
case P2IV_P2IFG1: break;
case P2IV_P2IFG2: break;
case P2IV_P2IFG3:
g_touch_event_cnt++;
P2IFG &= ~TOUCH_INT;
break;
case P2IV_P2IFG4: break;
case P2IV_P2IFG5: break;
case P2IV_P2IFG6: break;
case P2IV_P2IFG7: break;
default: break;
}
}
坐标读取流程
- 检测当前触摸事件技术,为0则立即退出
- 读取COOR_ADDR寄存器,获得当前的触屏是否有坐标数据,有几个手指的坐标数据
- 如果COOR_ADDR寄存器的bit7为0,说明没数据,立即向COOR_ADDR写0,使得GT911恢复工作
- 再检查COOR_ADDR的bit[3:0],为0说明所有手指离开,如果不为0,说明有手指触摸
- 读取从0x814F(COOR_DATA)寄存器开始的8*N个手指的坐标数据
- 向COOR_ADDR写0,使得GT911恢复工作
8字节坐标数据的格式:
/* 读取触摸点坐标数据,从0x814F寄存器开始读取
* 其中每一个触摸点使用8个寄存器来描述
* 以第一个触摸点为例,各寄存器描述信息如下:
* 0x814F: 触摸点id
* 0x8150: 触摸点X轴坐标低位字节
* 0x8151: 触摸点X轴坐标高位字节
* 0x8152: 触摸点Y轴坐标低位字节
* 0x8153: 触摸点Y轴坐标高位字节
* 0x8154~0x8155: 触摸点的大小信息,我们不需要
* 0x8156: 保留
*/
主机侧的Linux接收流程
这块交给外包做了,我只做了些指导,略。
电容屏的数据上报到主机侧
使用SPI上报,因为之前的SPI口仅用于供电管理,因为可扩展性非常差,为了复用SPI通道及其通信代码,对相关数据结构和收发函数做了优化:
SPI数据结构的扩展
typedef struct
{
uint8_t cmd; /*命令码控制字*/
union {
struct {
uint8_t dc:1; /*适配器在位状态*/
uint8_t bat: 1; /*电池在位状态*/
uint8_t full: 1; /*电池电量满状态*/
uint8_t ad: 1; /*电池电量数据模式*/
uint8_t reserve_4: 1; /*保留位4*/
uint8_t reserve_5: 1; /*保留位5*/
uint8_t reserve_6: 1; /*保留位6*/
uint8_t pd: 1; /*操作电源*/
uint8_t battery_data; /*电池电量数据*/
};
struct {
uint8_t state;
uint8_t x_l;
uint8_t x_h;
uint8_t y_l;
uint8_t y_h;
uint8_t rsv1;
uint8_t rsv2;
};
uint8_t data[7];
};
} spi_pack;
为了最小化扩展带来的代码改动,用到了匿名联合体
和匿名结构体
,这个在我之前的文章中有描述。
供电管理的SPI上报流程
uint8_t power_report(uint8_t opt)
{
spi_pack power_pack = {0};
power_pack.cmd = SPI_CMD_WRITE;
if(opt == POWER_DOWN)
{
power_pack.pd = POWER_DOWN;
} else {
power_pack.pd = POWER_NORMAL;
}
DEBUG("spi-0:0x%02x, 1:0x%02x, 2:0x%02x\r\n",power_pack.cmd,power_pack.data[0],power_pack.data[1]);
spi_sent(&power_pack, sizeof(power_pack));
}
触摸坐标的SPI上报流程
static int touch_report(uint8_t state, uint16_t x, uint16_t y)
{
spi_pack touch_pack = {0};
touch_pack.cmd = SPI_CMD_TOUCH;
touch_pack.state = state;
touch_pack.x_l = x & 0xff;
touch_pack.x_h = (x >> 8) & 0xff;
touch_pack.y_l = y & 0xff;
touch_pack.y_h = (y >> 8) & 0xff;
DEBUG("spi-touch: 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x\r\n",
touch_pack.cmd, touch_pack.data[0], touch_pack.data[1],
touch_pack.data[2], touch_pack.data[3], touch_pack.data[4]);
spi_sent(&touch_pack, sizeof(touch_pack));
return 0;
}
最终效果
在调通流程后,发现默认配置参数存在坐标x轴和y轴弄反、触摸灵敏度差等问题,反馈给FAE后,更新了一版配置参数,效果好多了。
F581触屏
总结
- 裸机开发在功能较多时非常不便,最好先移植个RTOS,磨刀不误砍柴工。
- 不要因为可以马上开始就选择步行,下地库取车能更快抵达目的地。