一、顺序与屏障
在《Linux 内核设计与实现》中展示了一个代码实例,在某些处理器上存在以下代码:
a = 1;
b = 2;
编译器和处理器为了提高效率,可能对读和写重新排序,像上面的就可能会出现,在给a中赋值之前就在b中存放新值,这样就使问题变得复杂化了。如果是下面这种具有依赖关系的,就不会发生这种问题。
a = 1;
b = a;
为了确保顺序要求,内核提供了一些接口来保证指令的执行顺序,这些顺序的指令称为屏障。
rmb():读屏障函数。在它之前的读操作必须全部完成,才能执行它之后的读操作。
wmb():写屏障函数。在它之前的写操作必须全部完成,才能执行它之后的写操作。
mb ():全能屏障函数。在它之前的读写操作必须全部完成,才能执行它之后的读写操作。
isb():最严格,是一种指令级别的内存屏障,用于确保指令执行的顺序性和可见性,一般在ARM架构的处理器中使用。它会刷新处理器流水线,确保在isb()指令之前的所有指令都已完成,并且对共享数据的修改在isb()指令之后对其它处理器可见。
需要注意的是,isb()指令的开销较高,会导致处理器流水线的刷新和停顿,因此在代码中应该谨慎使用,只在必要的情况下才使用。
实际上内存屏障问题的出现在非多线程的项目中并不多见,但一但遇见了也是很难排查的一个问题,下面将分享一个本人遇到的一个问题。
二、问题背景
终端设备长时间静拷,有概率性出现黑屏,较长时间才能复现。背光的控制单元是一个I2C设备,主控通过模拟I2C通信写寄存器来控制背光亮度,并对上层业务提供对应的API。在出问题时,通过读取背光亮点的寄存器,得到值被修改了。
但经过前面相关技术人员的排查,基本上确认了业务和驱动侧都不会主动去配置该寄存器。
I2C设备内部抓信号反馈信息:总线上的波形有的,现象也很明显,总线上发生了0x07的写,在ACK阶段中断挂起了,下一次轮询0x05导致0x08读寄存器写入了0x05,导致黑屏。
上面就是基本的问题情况,很难去定位具体的问题原因所在。
三、调试流程
1. 读写测试
想到做读写强度测试,还是因为设备拷机4~5天没有出问题,并且出问题时抓的信息依旧很难去定位问题(I2C设备内部检测0x7寄存器被修改,会给主控一个GPIO跳变信号触发中断,以此来记录当时现场,但很难去解决问题),所以尝试去反复读写I2C设备,看看是否能找到一些异常。
经过循环读写测试I2C设备某个寄存器,发现会出现报错问题,对比模拟I2C的代码之后,表现出来的是无 NACK,及当主控将8bit 数据发送给I2C 从设备时,从设备没有去应答。
在这之前做了很多其它工作包括 i2c 的读写一次操作所需要花费的时间等,均没有问题。
从黑屏问题转到 i2c 读写问题,基本上可以认为是同一个问题。
2. 确认SDA\SCL GPIO 状态
因为是模拟I2C,所以需要确认出问题时当前GPIO的状态与代码是否一致。
通过 /sys/class/gpio/ 节点下的相关属性,来确认出问题时 SDA SCL 出问题后的方向,以及 value,与代码对比之后,是一致的,说明SDA 的输入输出方向是正常的,电平配置也正常,并且获取 value 的值也是1,意味着外部给的是高电平,符合报错时的场景。并且尝试多次获取 sda 的电平,均未检测到低电平。
3. 测量i2c信号
测量信号之前需要先写测试程序,使用一个GPIO做触发信号,需要能够让示波器及时抓取出问题时的信号,抓取的信号如下:
很明显,问题在
可以看到在数据发送的过程中,SCL信号的建立出现了重叠。对比代码分析,以下为简略代码
for(i=0; i<8; i++) {
udelay(2);
tmp_bit = ((data<<i)&0x80) >> 7;
sda(busid, tmp_bit);
udelay(3);
scl(busid, 1);
scl(busid, 0);
}
这个写 tmp_bit后这个 udelay(3) 信号上并没有直观体现出来,尝试延时5us也是一样,正常 scl 应该在延后3us再置高。
udelay 有概率不生效?
3. udelay 间隔测试
#include <linux/time.h>
#include <linux/rtc.h>
#include <linux/ktime.h>
static void timetest(s64 *timediff)
{
struct timespec64 befTimeTs, curTimeTs;;
s64 ctime, btime;
ktime_get_boottime_ts64(&befTimeTs);
udelay(2);
ktime_get_boottime_ts64(&curTimeTs);
s64 sec_diff = curTimeTs.tv_sec - befTimeTs.tv_sec;
s64 nsec_diff = curTimeTs.tv_nsec - befTimeTs.tv_nsec;
if (nsec_diff < 0) {
nsec_diff += 1000000000; // 加上一秒的纳秒数量
sec_diff--; // 秒数减一
}
*timediff = sec_diff * 1000000000 + nsec_diff;
//printk("bef time: %lld s %ld ns\n", befTimeTs.tv_sec, befTimeTs.tv_nsec);
//printk("cur time: %lld s %ld ns\n", curTimeTs.tv_sec, curTimeTs.tv_nsec);
//printk("dif time: %lld s %ld ns\n", sec_diff, nsec_diff);
//printk("dif time: %lld ns\n", *timediff);
}
case 9:
{
s64 timediff, maxtime, mintime;
u64 count;
timetest(&maxtime);
timetest(&mintime);
if(maxtime < mintime){
timediff = maxtime;
maxtime = mintime;
mintime = timediff;
}
printk("init, maxtime=%lld ns, mintime=%lld ns,\n", maxtime, mintime);
count = 5000000;
for(i = 0; i < count; i++) {
timetest(&timediff);
if(timediff < mintime)
mintime = timediff;
else if(timediff > maxtime)
maxtime = timediff;
else
timediff = 0;
}
printk("test, maxtime=%lld ns, mintime=%lld ns, count=%d,\n", maxtime, mintime, count);
break;
}
测试结果如下
测试 udelay(2) 200w 次,看最低值都不会小于这个 2us,说明 udelay 没有问题
进一步确认,在 i2c 函数下判断
static void timetest()
{
struct timespec64 befTimeTs, curTimeTs;;
s64 ctime, btime, timediff ;
ktime_get_boottime_ts64(&befTimeTs);
udelay(2);
ktime_get_boottime_ts64(&curTimeTs);
s64 sec_diff = curTimeTs.tv_sec - befTimeTs.tv_sec;
s64 nsec_diff = curTimeTs.tv_nsec - befTimeTs.tv_nsec;
if (nsec_diff < 0) {
nsec_diff += 1000000000; // 加上一秒的纳秒数量
sec_diff--; // 秒数减一
}
timediff = sec_diff * 1000000000 + nsec_diff;
if(timediff < 3000)
printk("dif time : %llds ns\n", timediff);
//printk("bef time: %lld s %ld ns\n", befTimeTs.tv_sec, befTimeTs.tv_nsec);
//printk("cur time: %lld s %ld ns\n", curTimeTs.tv_sec, curTimeTs.tv_nsec);
//printk("dif time: %lld s %ld ns\n", sec_diff, nsec_diff);
//printk("dif time: %lld ns\n", *timediff);
}
for(i=0; i<8; i++) {
udelay(2);
tmp_bit = ((data<<i)&0x80) >> 7;
sda(busid, tmp_bit);
//udelay(3);
timetest();
scl(busid, 1);
scl(busid, 0);
}
未有相关打印出现,说明udelay是ok的
总结:目前很难确认原因在哪里,因为udelay生效了,并且gpio的变换也是和代码一致的,唯一的怀疑点只能是延时先于写bit 生效,也就是重新排序了。
四、问题原因
当知道出问题的原因之后,解决问题就很容易了,在 udelay 前后均加上 mb() 函数,来确保每一个 GPIO 读写操作在完成之后在延时,并且延时生效之后,再进行 GPIO 读写操作,经拷机验证,问题得到解决。