Yuhang Zheng

Linux系统实现SATA热插拔功能

N 人看过

日前,工作中遇到了一个SATA热插拔的需求,客户想要在系统使用过程中能够通过一个开关来做控制,支持在不重启系统的情况下实现SATA硬盘的热插拔。

为了实现此功能,硬件设置如下:

GPIO2_3用于一个船型开关,开关闭合(船型开关置“1”)时与GND短路。
SATA_PWRLED连接LED,指示SATA电源,点亮表示SATA供电。
SATA_STALED连接LED,指示硬盘运行状态。

概况来说,就是有以下资源:

一个DI输入,接了一个船型开关,可以通过拨动开关来获取01值
一个DO输出,控制着SATA硬盘的电源,并连接了一个LED灯
一个DO输出,连接了一个LED灯,可以配置成指示SATA硬盘的读写状态(与实现SATA热插拔功能无关)

软件上想要实现的逻辑如下:

插入硬盘:

在SATA电源灯熄灭状态下,插入硬盘
拨动船型开关置“1”
SATA电源灯亮起,状态灯闪烁
硬盘正常工作

拔出硬盘:

拨动船型开关置“0”
稍等片刻,待SATA电源灯熄灭
拔出硬盘即可

首先我在网上搜索了一下linux系统下的SATA硬盘热插拔的命令,找到了这篇文章《Linux上磁盘热插拔》。从这篇文章中获取了下面两个命令

#SATA硬盘热拔,删除该设备
echo 1 > /sys/bus/scsi/drivers/sd/0\:0\:0\:0/delete
#SATA硬盘热插,通过扫描scsi总线实现热插功能
echo "- - -" >  /sys/class/scsi_host/host0/scan

我在板卡上测试了一下,确实可以通过上面的命令来实现SATA的热插拔,那么下面的问题就是如何通过一个船型开关来控制执行这两条命令。

最简单的方法就是写一个shell脚本,通过不断轮询船型开关的DI输入状态来执行这两条命令。但是这样如果想要保持及时性,就会大量的占用CPU资源,至少会导致一个CPU核心一直处于满负载状态。

这个时候我们当然就想到了使用中断来实现此功能,在内核中注册一个驱动,使用等待队列的知识,然后在文件系统中使用阻塞读即可。

后来在网上搜索方法,我们找到了一个文件系统层使用CPU中断的方法,文件为《linux用户态使用gpio中断方法》。最终结合我们的需求,最终的程序如下:

#include <stdio.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    int pov;
    int ov = 1;

    system("echo both > /sys/class/gpio/gpio451/edge");
    int fd=open("/sys/class/gpio/gpio451/value",O_RDONLY);
    if(fd<0)
    {
        perror("open '/sys/class/gpio/gpio451/value' failed!\n");
        return -1;
    }
    struct pollfd fds[1];
    fds[0].fd=fd;
    fds[0].events=POLLPRI;
    while(1)
    {
        if(poll(fds,1,-1)==-1)
        {
            perror("poll failed!\n");
            return -1;
        }
        if(fds[0].revents&POLLPRI)
        {
            if(lseek(fd,0,SEEK_SET)==-1)
            {
                perror("lseek failed!\n");
                return -1;
            }
            char buffer[16];
            int len;
            if((len=read(fd,buffer,sizeof(buffer)))==-1)
            {
                perror("read failed!\n");
                return -1;
            }
            buffer[len]=0;

            if((int)buffer[0] == 48)
            {
                if(!ov)
                {
                    ov = 1;
                    system("echo Connect SATA ...");
                    system("echo 1 > /sys/class/leds/SATA_PWRON/brightness");
                    sleep(1);
                    system("echo '- - -' >  /sys/class/scsi_host/host0/scan");
                }
            }
            else
            {
                if(ov)
                {
                    ov = 0;
                    system("echo DISCONNECT SATA ...");
                    system("umount /dev/sda1");
                    system("echo 1 > /sys/bus/scsi/drivers/sd/0:0:0:0/delete");
                    system("echo 0 > /sys/class/leds/SATA_PWRON/brightness");
                }
            }
        }
    }
    return 0;
}

这里需要注意的一些逻辑是:

在执行SATA上电的操作后,最好等待一些时间再执行SATA总线的扫描操作

在执行SATA的删除操作前,最好先将SATA卸载掉

当然上面写的程序也不是完美逻辑的,只是为以后结果类似问题提供了一些参考。

最后,在文件系统创建自启服务如下:

rootfs_ubuntu_bionic_arm64/etc/systemd/system/sata_deamon.service

[Unit]
Description = sata deamon

[Service]
ExecStart = /usr/sbin/sata_deamon
Restart = always
ExecStartPre=/bin/sleep 5
Type = simple

[Install]
WantedBy = multi-user.target

另外,如果想使用一个led灯来指示SATA的硬盘的读写状态的话,可以在内核的defconfig上配置上以下模块

CONFIG_LEDS_TRIGGER_DISK=y

另外在内核设备树中配置以下代码即可

        leds {
                status = "okay";
                compatible = "gpio-leds";

                SATA_STALED {
                        label = "SATA_STALED";
                        gpios = <&gpio1 15 1>;
                        linux,default-trigger = "disk-activity";
                };
             };