Linux 移动硬盘安全关闭

最近固态价格越来越便宜了,就买了一个航雄M2移动硬盘盒,方案是 RTL9120B,支持 Nvme/Sata 双协议,然后弄了一块 1T 的大杯固态。然后把用了2年多的 U盘 Debian系统迁移过去。具体迁移方法过后再写篇文章仔细描述,GPT/UEFI 系统迁移实在是太简单了。

之前用的 U盘,没有 SMART 信息,换移动硬盘后就发现,当笔记本关机后,移动硬盘还是工作状态,我这个硬盘盒指示灯是双色,绿色代表工作(总线桥接中),蓝色代表待机(Nvme电源关闭),关机后直到电脑 USB 自动断电,硬盘盒断电,再进入系统看 SMART 就可以看到“不安全关机(Unsafe Shutown Counts”会递增 1。虽然固态硬盘这个计数实际不会损害,但是过于膈应,而且我觉得系统本身不可能不支持自动给移动硬盘自动弹出。

这个跟旧固件的 RTK9210B 硬盘盒 bug 不同,我的硬盘盒固件是比较新的 1.31,弹出硬盘不安全关机次数递增的 bug 已经修正了。我这种情况是 Linux 系统装在移动硬盘上,然后关机时系统没有通知 USB 设备进入待机(Standby)状态。查询了许多资料,都是让在 /lib/systemd/system-shutdown 中使用 udisksctl 或者 hdparm 去卸载/关闭 USB 存储,实际操作下来并没有什么用,直到看到论坛帖子 Graceful way to spin down (unload head) USB attached mobile hard drive BEFRORE shutdown(poweroff/reboot/halt),才找到正确的解决方案。

具体就是,需要使能系统 scsi_disk 模块对于 USB 存储的 manage_start_stop 标志,这样系统就会管理这些设备的启动和停止任务。对于一般的外置移动硬盘而言,通常是用户自行加载移除设备,但是对于运行了系统的移动硬盘而言,用户无法自行处理移除,需要系统自己在关机时处理。比较奇怪的是为什么 Linux 没有默认使能这个标志。

解决方案:

  1. 手动处理(需要root权限,重启失效):
1
2
3
4
# 使能 manage_start_stop, [...] 为硬盘节点,可以补全
echo 1 | sudo tee /sys/class/scsi_disk/[...]/manage_start_stop
# 检查
cat /sys/class/scsi_disk/[...]/manage_start_stop
  1. 利用 udev 规则,自动处理:
1
2
# 创建文件 /etc/udev/rules.d/01-manage-start-stop.rules
echo 'ACTION=="add|change", DRIVERS=="usb-storage|uas", SUBSYSTEM=="scsi_disk", ATTR{manage_start_stop}="1"' | sudo tee /etc/udev/rules.d/01-manage-start-stop.rules

另外,对于使用 RTL9210b 方案,Linux 系统默认状态下固态并不支持 TRIM 指令,可以通过一个 udev 规则来默认激活 TRIM 支持:

1
echo 'ACTION=="add|change", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="9210", SUBSYSTEM=="scsi_disk", ATTR{provisioning_mode}="unmap"' | sudo tee /etc/udev/rules.d/10-uas-discard.rules

然后通过命令 lsblk -D 检查 TRIM 支持:

1
2
3
4
5
6
7
$ lsblk -D
NAME        DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sda                0      512B       4G         0
├─sda1             0      512B       4G         0
├─sda2             0      512B       4G         0
├─sda3             0      512B       4G         0
└─sda4             0      512B       4G         0

Linux 下 Logitech G29 编程

本文介绍在 Linux 下 Logitech G29 套件的编程接入,包括摇杆数据读取、自动归中配置以及 LED 控制。

Linux 内核对于摇杆输入处理分几个部分,其中数据读取是 Kernel Joystick API, 设备是 /dev/input/jsX,对于 G29 而言,可以通过 /dev/input/by-id 来获取节点名,通常为 /dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-joystick。

而反馈,是 Kernel Force feeback API,设备是 /dev/input/eventX,也可以通过 /dev/input/by-id 来获取节点名。

这两部分参考内核文档即可读取传感器数据和设置反馈归中等效果。G29上还带了一个 LED 带,通常用于游戏中的视觉特效,比如发动机转速等。这个 LEDs 在内核中不属于通用设备,没有特定 API 支持,映射的设备文件为 /dev/hidrawX,可以通过 hidraw 与之通信,来控制 LEDs 状态,这部分可以参考 Logitech G29 Shifter LEDs. 需要注意的是,需要设定 udev 规则来配置该 hidrawX 的写入权限,因为默认该设备所属用户组均为 root。

完整样例代码如下,请注意需要将 G29 配置成 PS3 模式(G29在不同配置模式下,生成的 USB ID不同)。需要内核支持 Logitech 驱动,一般桌面 Linux 发行版已经打开了对应编译开关。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include <stdio.h>
#include <linux/joystick.h>
#include <linux/input.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdlib.h>

#include <libudev.h>


static sig_atomic_t m_signal = 0;
static void signal_cb(int sig)
{
    m_signal = sig;
    signal(sig, signal_cb);
}

static unsigned char LED_ON[] =   { 0xf8, 0x12, 0x1f, 0x00, 0x00, 0x00, 0x01 };
static unsigned char LED_OFF[] =  { 0xf8, 0x12, 0x00, 0x00, 0x00, 0x00, 0x01 };


static int open_g29_led()
{
    struct udev* udev = udev_new();
    if (!udev) {
        printf("udev_new() failed\n");
        return -1;
    }
    int hid = -1;
    struct udev_enumerate *enumerate = udev_enumerate_new(udev);
    udev_enumerate_add_match_subsystem(enumerate, "hidraw");
    udev_enumerate_scan_devices(enumerate);

    struct udev_list_entry *devices = udev_enumerate_get_list_entry(enumerate);
    struct udev_list_entry *entry;
    udev_list_entry_foreach(entry, devices) {
        const char *path = udev_list_entry_get_name(entry);
        struct udev_device *dev = udev_device_new_from_syspath(udev, path);
        if (dev) {
            struct udev_device *hid_dev =
                udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_device");
            if (hid_dev) {
                const char *node = udev_device_get_devnode(dev); // devnode should be hidraw, not usb
                const char *vendor = udev_device_get_sysattr_value(hid_dev, "idVendor");
                const char *product = udev_device_get_sysattr_value(hid_dev, "idProduct");
                if (node && vendor && product) {
                    printf("%s\t%s:%s\n", node, vendor, product);
                    if (strtol(vendor, NULL, 16) == 0x046d && strtol(product, NULL, 16) == 0xc24f) {
                        printf("g29 led hidraw path: %s\n", node);
                        hid = open(node, O_WRONLY);
                        if (hid == -1) {
                            perror("open() hidraw");
                        }
                        break;
                    }
                }
            }
            udev_device_unref(dev);
        }
    }

    udev_enumerate_unref(enumerate);
    udev_unref(udev);
    return hid;
}

int main(int argc, char **argv)
{
    signal(SIGTERM, signal_cb);
    signal(SIGINT, signal_cb);
    char *input = "/dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-joystick";
    char *event = "/dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-event-joystick";
    if (argc > 1) {
        input = argv[1];
    }
    if (argc > 2) {
        event = argv[2];
    }
    printf("device: %s\n", input);
    printf("event: %s\n", event);
    open_g29_led();
    int fd = open(input, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    int ev = open(event, O_RDWR);
    if (ev == -1) {
        close(fd);
        perror("open");
        return -1;
    }

    int autocenter = 65;
    struct input_event ie;
    ie.type = EV_FF;
    ie.code = FF_AUTOCENTER;
    ie.value = 0xFFFFUL * autocenter / 100;
    if (write(ev, &ie, sizeof(ie)) == -1) {
        perror("set auto-center");
    }

    int led = open_g29_led();
    if (led == -1) {
        printf("open led failed, ignore LEDs\n");
    }
    if (led != -1) {
        write(led, LED_ON, sizeof(LED_ON));
    }
    puts("Hello, world!");
    sleep(1);

    while (m_signal == 0) {
        fd_set fds;
        FD_ZERO(&fds);
        FD_SET(fd, &fds);
        struct timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = 50000;
        int rv = select(fd + 1, &fds, NULL, NULL, &tv);
        if (rv == -1) {
            perror("select");
            break;
        }
        if (rv == 0) {
            continue;
        }

        struct js_event e;
        int r = read(fd, &e, sizeof(e));
        if (r == -1) {
            perror("read");
            break;
        } else if (r == 0) {
            printf("!!!\n");
        } else {
            int init = (e.type & JS_EVENT_INIT) != 0;
            e.type &= ~JS_EVENT_INIT;
            printf("%.3lf %d\t%d\t%d\t%d\n", e.time * 1E-3, init,  e.type, e.number, e.value);
        }
    }
    printf("signal : %s\n", strsignal(m_signal));

    if (led != -1) {
        write(led, LED_OFF, sizeof(LED_OFF));
        close(led);
    }

    ie.type = EV_FF;
    ie.code = FF_AUTOCENTER;
    ie.value = 0xFFFFUL * 0 / 100;
    if (write(ev, &ie, sizeof(ie)) == -1) {
        perror("set auto-center");
    }

    close(fd);
    close(ev);
    return 0;
}

备注,对于 LED 控制权限问题,可以通过 udev rule 来配置:

1
ACTION=="add",SUBSYSTEMS=="usb",ATTRS{idVendor}=="046d",ATTRS{idProduct}=="c24f",GROUP="users",MODE="0666"

FTDI 串口驱动数据包延迟

最近项目测试发现串口收取时存在延迟、拥堵现象,多个 packet 接收的时间戳是一样的,但是发送的时候的确是每帧间隔固定时间。

测试了手头的 USB转串口模块,CH340的没有这个问题,但是 FT 系列都存在这个问题,包括 FT232/FT2232 等。现象是高频小包数据通信时,多个包拥堵到达,达到时间是一样的,但是发送时候是明显单包单包发送,就好像传输过来有缓冲区一样。

搜索了 “FTDI+Latency"后,发现如果使用的是 FTDI 厂商的 USB 转 UART 串口模块,默认是存在传输延迟的,参见 FTDI Linux USB latency,通过一下命令可以查看当前的默认延迟时间,默认一般为 16ms,这个对于高频率小包传输的影响是相当明显的:

1
cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timer

对于目前的新版内科而言,命令 echo 1 > /sys/bus/usb-serial/devices/ttyUSB0/latency_timer 无法使用,提示 Input/Output Error,正确的方法是通过系统 API 来配置串口描述符 的 Low Latency 属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <sys/ioctl.h>
#include <linux/serial.h>
...
// low latency
int serial = open(...);
...
struct serial_struct kernel_serial_settings;
ioctl(serial, TIOCGSERIAL, &kernel_serial_settings);
kernel_serial_settings.flags |= ASYNC_LOW_LATENCY;
ioctl(serial, TIOCSSERIAL, &kernel_serial_settings);

如此设置后,内核会通知 ftdi 驱动部分去降低 latency_timer 值,设置后可以通过 cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timer 查看到该值已变更为 1.