【问题记录】内存屏障及实例分析

一、顺序与屏障

在《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 读写操作,经拷机验证,问题得到解决。

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值