大佬,rtsp_lib 是你写的吗?有些眼熟哇。
dream 发布的帖子
-
回复: 全志D1裸奔工具XFEL
@dsp2000 通过 Zadig 工具 把 "USB Device(VID_1f3a_PID_efe8)" 编辑名称为 "Allwinner SoC in FEL mode" 就行了。
-
回复: V851S Tina 5.0 SDK 无法使用文件锁的问题
@dream 同步最新的信息, 已经使用了 mutex 的方式,替换文件锁,可以解决 opkg remove 和 opkg list 等指令不能正常使用的问题,目前 opkg install 会出现段错误。
原以为解决了文件锁,就能解决 opkg 不能使用的问题,没想到只解决了一半
*** libopkg\opkg_conf.c 2022-02-25 04:27:58.000000000 +0800 --- libopkg\opkg_conf.c 2024-03-03 14:20:36.000000000 +0800 *************** *** 19,30 **** --- 19,31 ---- #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <glob.h> #include <unistd.h> + #include <sys/mman.h> #include "opkg_conf.h" #include "pkg_vec.h" #include "pkg.h" #include "xregex.h" #include "sprintf_alloc.h" *************** *** 423,436 **** --- 424,452 ---- int opkg_conf_init(void) { pkg_src_list_init(&conf->pkg_src_list); pkg_dest_list_init(&conf->pkg_dest_list); pkg_dest_list_init(&conf->tmp_dest_list); nv_pair_list_init(&conf->arch_list); + + conf->mlock = mmap(NULL, sizeof(struct mt_lock), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0); + memset(conf->mlock, 0, sizeof(struct mt_lock)); + pthread_mutexattr_init(&conf->mlock->mutexattr); + pthread_mutexattr_setpshared(&conf->mlock->mutexattr, PTHREAD_PROCESS_SHARED); + pthread_mutex_init(&conf->mlock->mutex, &conf->mlock->mutexattr); return 0; + } + + static int my_lockf(int fd, int cmd, off_t len) + { + switch(cmd){ + case F_TLOCK: return pthread_mutex_trylock(&conf->mlock->mutex); + case F_ULOCK: return pthread_mutex_unlock(&conf->mlock->mutex); + } + return -22; } int opkg_conf_load(void) { int i, glob_ret; char *tmp, *tmp_dir_base, **tmp_val; *************** *** 498,510 **** lock_fd = creat(lock_file, S_IRUSR | S_IWUSR | S_IRGRP); if (lock_fd == -1) { opkg_perror(ERROR, "Could not create lock file %s", lock_file); goto err2; } ! if (lockf(lock_fd, F_TLOCK, (off_t) 0) == -1) { opkg_perror(ERROR, "Could not lock %s", lock_file); if (close(lock_fd) == -1) opkg_perror(ERROR, "Couldn't close descriptor %d (%s)", lock_fd, lock_file); lock_fd = -1; goto err2; --- 514,526 ---- lock_fd = creat(lock_file, S_IRUSR | S_IWUSR | S_IRGRP); if (lock_fd == -1) { opkg_perror(ERROR, "Could not create lock file %s", lock_file); goto err2; } ! if (my_lockf(lock_fd, F_TLOCK, (off_t) 0) == -1) { opkg_perror(ERROR, "Could not lock %s", lock_file); if (close(lock_fd) == -1) opkg_perror(ERROR, "Couldn't close descriptor %d (%s)", lock_fd, lock_file); lock_fd = -1; goto err2; *************** *** 573,585 **** hash_table_deinit(&conf->file_hash); hash_table_deinit(&conf->obs_file_hash); if (rmdir(conf->tmp_dir) == -1) opkg_perror(ERROR, "Couldn't remove dir %s", conf->tmp_dir); err3: ! if (lockf(lock_fd, F_ULOCK, (off_t) 0) == -1) opkg_perror(ERROR, "Couldn't unlock %s", lock_file); if (close(lock_fd) == -1) opkg_perror(ERROR, "Couldn't close descriptor %d (%s)", lock_fd, lock_file); if (unlink(lock_file) == -1) --- 589,601 ---- hash_table_deinit(&conf->file_hash); hash_table_deinit(&conf->obs_file_hash); if (rmdir(conf->tmp_dir) == -1) opkg_perror(ERROR, "Couldn't remove dir %s", conf->tmp_dir); err3: ! if (my_lockf(lock_fd, F_ULOCK, (off_t) 0) == -1) opkg_perror(ERROR, "Couldn't unlock %s", lock_file); if (close(lock_fd) == -1) opkg_perror(ERROR, "Couldn't close descriptor %d (%s)", lock_fd, lock_file); if (unlink(lock_file) == -1) *************** *** 652,664 **** pkg_hash_deinit(); hash_table_deinit(&conf->file_hash); hash_table_deinit(&conf->obs_file_hash); if (lock_fd != -1) { ! if (lockf(lock_fd, F_ULOCK, (off_t) 0) == -1) opkg_perror(ERROR, "Couldn't unlock %s", lock_file); if (close(lock_fd) == -1) opkg_perror(ERROR, "Couldn't close descriptor %d (%s)", lock_fd, lock_file); --- 668,680 ---- pkg_hash_deinit(); hash_table_deinit(&conf->file_hash); hash_table_deinit(&conf->obs_file_hash); if (lock_fd != -1) { ! if (my_lockf(lock_fd, F_ULOCK, (off_t) 0) == -1) opkg_perror(ERROR, "Couldn't unlock %s", lock_file); if (close(lock_fd) == -1) opkg_perror(ERROR, "Couldn't close descriptor %d (%s)", lock_fd, lock_file); *** libopkg\opkg_conf.h 2022-02-25 04:27:58.000000000 +0800 --- libopkg\opkg_conf.h 2024-03-03 14:20:05.000000000 +0800 *************** *** 20,31 **** --- 20,32 ---- typedef struct opkg_conf opkg_conf_t; extern opkg_conf_t *conf; #include <stdarg.h> #include <fnmatch.h> /* FNM_CASEFOLD */ + #include <pthread.h> #include "hash_table.h" #include "pkg_src_list.h" #include "pkg_dest_list.h" #include "nv_pair_list.h" *************** *** 39,50 **** --- 40,56 ---- /* In case the config file defines no dest */ #define OPKG_CONF_DEFAULT_DEST_NAME "root" #define OPKG_CONF_DEFAULT_DEST_ROOT_DIR "/" #define OPKG_CONF_DEFAULT_HASH_LEN 1024 + + struct mt_lock { + pthread_mutex_t mutex; + pthread_mutexattr_t mutexattr; + }; struct opkg_conf { pkg_src_list_t pkg_src_list; pkg_dest_list_t pkg_dest_list; pkg_dest_list_t tmp_dest_list; nv_pair_list_t arch_list; *************** *** 105,116 **** --- 111,125 ---- char *signature_ca_file; char *signature_ca_path; hash_table_t pkg_hash; hash_table_t file_hash; hash_table_t obs_file_hash; + + // replace file lock + struct mt_lock *mlock; }; enum opkg_option_type { OPKG_OPT_TYPE_BOOL, OPKG_OPT_TYPE_INT, OPKG_OPT_TYPE_STRING
-
V851S Tina 5.0 SDK 无法使用文件锁的问题
起初是为了分析 opkg 工具用不了的问题:
root@TinaLinux:/# opkg list Collected errors: * opkg_conf_load: Could not lock /var/lock/opkg.lock: Permission denied.
通过分析其系统调用链,发现 opkg 试图设置文件锁失败了:
root@TinaLinux:/# strace opkg list execve("/bin/opkg", ["opkg", "list"], 0xbef8ad04 /* 21 vars */) = 0 set_tls(0xb6f555d8) = 0 set_tid_address(0xb6f5618c) = 1120 open("/usr/lib/eyesee-mpp/libubox.so.20210516", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/usr/lib/eyesee-mpp/libubox.so.20210516", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/etc/ld-musl-armhf.path", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/lib/libubox.so.20210516", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3 fcntl64(3, F_SETFD, FD_CLOEXEC) = 0 fstat64(3, {st_mode=S_IFREG|0644, st_size=38084, ...}) = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\244!\0\0004\0\0\0"..., 936) = 936 mmap2(NULL, 106496, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xb6ec0000 mmap2(0xb6ed8000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x8000) = 0xb6ed8000 close(3) = 0 mprotect(0xb6ed8000, 4096, PROT_READ) = 0 mprotect(0x3e000, 4096, PROT_READ) = 0 stat64("/etc/opkg.conf", {st_mode=S_IFREG|0644, st_size=85, ...}) = 0 open("/etc/opkg.conf", O_RDONLY|O_LARGEFILE) = 3 brk(NULL) = 0xd97000 brk(0xd98000) = 0xd98000 brk(0xd99000) = 0xd99000 brk(0xd9a000) = 0xd9a000 brk(0xd9b000) = 0xd9b000 brk(0xd9c000) = 0xd9c000 brk(0xd9d000) = 0xd9d000 brk(0xd9f000) = 0xd9f000 brk(0xda0000) = 0xda0000 read(3, "dest root /\ndest ram /tmp\nlists_"..., 1024) = 85 read(3, "", 1024) = 0 close(3) = 0 open("/etc/opkg/", O_RDONLY|O_LARGEFILE|O_CLOEXEC|O_DIRECTORY) = 3 fcntl64(3, F_SETFD, FD_CLOEXEC) = 0 getdents64(3, 0xb6ed9378 /* 4 entries */, 2048) = 112 getdents64(3, 0xb6ed9378 /* 0 entries */, 2048) = 0 close(3) = 0 open("/etc/opkg/customfeeds.conf", O_RDONLY|O_LARGEFILE) = 3 read(3, "# add your custom package feeds "..., 1024) = 103 read(3, "", 1024) = 0 close(3) = 0 open("/var/lock/opkg.lock", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0640) = 3 fcntl64(3, F_SETLK64, {l_type=F_WRLCK, l_whence=SEEK_CUR, l_start=0, l_len=0}) = -1 EACCES (Permission denied) close(3) = 0 writev(2, [{iov_base="", iov_len=0}, {iov_base="Collected errors:\n", iov_len=18}], 2Collected errors: ) = 18 writev(2, [{iov_base=" * opkg_conf_load: Could not loc"..., iov_len=75}, {iov_base=NULL, iov_len=0}], 2 * opkg_conf_load: Could not lock /var/lock/opkg.lock: Permission denied. ) = 75 exit_group(-1) = ? +++ exited with 255 +++
然后自己写一个测试程序,确实会失败:
root@TinaLinux:/tmp# strace ./test execve("./test", ["./test"], 0xbeb32d00 /* 22 vars */) = 0 set_tls(0xb6f9c5d8) = 0 set_tid_address(0xb6f9d18c) = 1133 open("/usr/lib/eyesee-mpp/libstdc++.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/usr/lib/eyesee-mpp/libstdc++.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/etc/ld-musl-armhf.path", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/lib/libstdc++.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3 fcntl64(3, F_SETFD, FD_CLOEXEC) = 0 fstat64(3, {st_mode=S_IFREG|0775, st_size=976512, ...}) = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\340\2\7\0004\0\0\0"..., 936) = 936 mmap2(NULL, 1048576, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xb6e21000 mmap2(0xb6f18000, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xe7000) = 0xb6f18000 mmap2(0xb6f20000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb6f20000 close(3) = 0 open("/usr/lib/eyesee-mpp/libgcc_s.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/usr/lib/eyesee-mpp/libgcc_s.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/lib/libgcc_s.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3 fcntl64(3, F_SETFD, FD_CLOEXEC) = 0 fstat64(3, {st_mode=S_IFREG|0664, st_size=38356, ...}) = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\230'\0\0004\0\0\0"..., 936) = 936 mmap2(NULL, 106496, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xb6e07000 mmap2(0xb6e1f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x8000) = 0xb6e1f000 close(3) = 0 mprotect(0xb6f18000, 20480, PROT_READ) = 0 mprotect(0xb6e1f000, 4096, PROT_READ) = 0 mprotect(0x20000, 4096, PROT_READ) = 0 brk(NULL) = 0xc2f000 brk(0xc34000) = 0xc34000 open("/tmp/opkg.lock", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0644) = 3 fcntl64(3, F_SETLK64, {l_type=F_WRLCK, l_whence=SEEK_CUR, l_start=0, l_len=0}) = -1 EACCES (Permission denied) ioctl(1, TIOCGWINSZ, {ws_row=0, ws_col=0, ws_xpixel=0, ws_ypixel=0}) = 0 writev(1, [{iov_base="fd:[3] rc:[-1", iov_len=13}, {iov_base="]\n", iov_len=2}], 2fd:[3] rc:[-1] ) = 15 close(3) = 0 exit_group(0) = ? +++ exited with 0 +++
在 ubuntu 上测试实例代码是可以上锁的,说明用法没问题:
ubuntu@ubuntu1804:~/work/v85x-tina-open/openwrt/package/dream/test/src$ g++ -o main main.cpp ubuntu@ubuntu1804:~/work/v85x-tina-open/openwrt/package/dream/test/src$ ubuntu@ubuntu1804:~/work/v85x-tina-open/openwrt/package/dream/test/src$ ./main fd:[3] rc:[0] ubuntu@ubuntu1804:~/work/v85x-tina-open/openwrt/package/dream/test/src$
请问各位大佬,有谁知道是咋回事吗?(附文件系统类型)
root@TinaLinux:/tmp# df -Th Filesystem Type Size Used Available Use% Mounted on /dev/root squashfs 12.0M 12.0M 0 100% /rom devtmpfs devtmpfs 23.8M 0 23.8M 0% /dev tmpfs tmpfs 25.0M 16.0K 25.0M 0% /tmp /dev/by-name/UDISK ubifs 10.4M 36.0K 9.8M 0% /mnt/UDISK overlayfs:/mnt/UDISK/overlay overlay 10.4M 36.0K 9.8M 0% / tmpfs tmpfs 25.0M 0 25.0M 0% /run
-
回复: Yuzuki Lizard V851S 开发板 WIFI 功能异常
@null037 解决了,结合论坛的 SDK 以及 github 的 SDK,补充缺失的文件就行了。
-
Yuzuki Lizard V851S 开发板 WIFI 功能异常
没查出是什么原因,有哪位大神可以帮忙看看是什么问题?
源码来源:github docker [https://github.com/YuzukiHD/Yuzukilizard]
问题现象:使用未经修改的 SDK 编译出来的固件,无法正常扫描 SSID 和连接 WIFI。
已经排查:
- 确认有识别到 XR829。
- 确认有成功下载 firmware。
- 确认所选的配置是 40M,且 GPIO 配置正确。
- 使用 iw 和 wifi -s 都不能正常扫描 ssid。
- 使用 ifconfig waln0 可以正常的 up、down。
问题日志:以下是内核启动 WIFI 时的日志输出:
[ 5.782165] ======== XRADIO WIFI OPEN ======== [ 5.787181] [XRADIO] Driver Label:XR_V02.16.91 _HT40_01.33 Feb 17 2024 10:35:49 [ 5.795746] [XRADIO] Allocated hw_priv @ c37c6c80 [ 5.801281] [XRADIO_ERR] Access_file failed, path:/data/misc/wifi/xr_wifi.conf! [ 5.810623] sunxi-wlan soc@03000000:wlan@0: bus_index: 1 [ 5.926666] sunxi-mmc sdc1: sdc set ios:clk 0Hz bm PP pm UP vdd 21 width 1 timing LEGACY(SDR12) dt B [ 5.936923] [XRADIO] Detect SDIO card 1 [ 5.951799] sunxi-mmc sdc1: no vqmmc,Check if there is regulator [ 5.979134] sunxi-mmc sdc1: sdc set ios:clk 400000Hz bm PP pm ON vdd 21 width 1 timing LEGACY(SDR12) dt B [ 6.019771] sunxi-mmc sdc1: sdc set ios:clk 400000Hz bm PP pm ON vdd 21 width 1 timing LEGACY(SDR12) dt B [ 6.034411] open /dev/mtd1 failed,ret=-6 [ 6.034423] remoteproc0: Can't finded boot_package head [ 6.034428] remoteproc0: sunxi_request_firmwarefailed,ret=-19 [ 6.054089] sunxi-mmc sdc1: sdc set ios:clk 400000Hz bm PP pm ON vdd 21 width 1 timing LEGACY(SDR12) dt B [ 6.075296] sunxi-mmc sdc1: sdc set ios:clk 400000Hz bm PP pm ON vdd 21 width 1 timing SD-HS(SDR25) dt B [ 6.086091] sunxi-mmc sdc1: sdc set ios:clk 50000000Hz bm PP pm ON vdd 21 width 1 timing SD-HS(SDR25) dt B [ 6.097147] sunxi-mmc sdc1: sdc set ios:clk 50000000Hz bm PP pm ON vdd 21 width 4 timing SD-HS(SDR25) dt B [ 6.108870] mmc1: new high speed SDIO card at address 0001 [ 6.115965] [SBUS] XRadio Device:sdio clk=50000000 [ 6.123030] [XRADIO] XRADIO_HW_REV 1.0 detected. [ 6.179491] [XRADIO] xradio_update_dpllctrl: DPLL_CTRL Sync=0x00c00000. [ 6.215546] [XRADIO] Bootloader complete [ 6.324725] [XRADIO] Firmware completed. [ 6.330654] [WSM] Firmware Label:XR_C09.08.52.73_DBG_02.122 2GHZ HT40 May 18 2021 13:36:09 [ 6.348111] [XRADIO] Firmware Startup Done. [ 6.353158] [XRADIO_WRN] enable Multi-Rx! [ 6.370051] ieee80211 phy0: Failed to initialize wep: -2 [ 6.376126] ieee80211 phy0: Selected rate control algorithm 'minstrel_ht' [ 6.472871] [VIN_WARN]sensor_helper_probe: cannot get sensor0_cameravdd supply, setting it to NULL! [ 6.483380] [VIN_WARN]sensor_helper_probe: cannot get sensor0_iovdd supply, setting it to NULL! [ 6.493411] [VIN_WARN]sensor_helper_probe: cannot get sensor0_avdd supply, setting it to NULL! [ 6.503160] [VIN_WARN]sensor_helper_probe: cannot get sensor0_dvdd supply, setting it to NULL! [ 6.583658] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 6.593242] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 6.602769] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 6.612213] [VIN_DEV_I2C]gc2053_mipi sensor read retry = 2 [ 6.819401] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 6.828941] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 6.838493] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 6.847865] [VIN_DEV_I2C]gc2053_mipi sensor read retry = 2 [ 7.074215] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 7.083765] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 7.093316] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 7.102687] [VIN_DEV_I2C]gc2053_mipi sensor read retry = 2 [ 7.309327] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 7.318844] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 7.328392] sunxi_i2c_do_xfer()1974 - [i2c1] incomplete xfer (status: 0x20, dev addr: 0x37) [ 7.337775] [VIN_DEV_I2C]gc2053_mipi sensor read retry = 2 [ 7.343932] [gc2053_mipi] error, chip found is not an target chip. [ 7.363360] [VIN_ERR]registering gc2053_mipi, No such device! [ 7.603549] udevd[893]: could not create /tmp/run/udev: No such file or directory [ 8.251755] file system registered [ 8.269359] configfs-gadget 4100000.udc-controller: failed to start g1: -19 [ 8.381933] read descriptors [ 8.385211] read strings [ 8.543205] sunxi_set_cur_vol_work()482 WARN: get power supply failed [ 8.630668] android_work: sent uevent USB_STATE=CONNECTED [ 8.814319] configfs-gadget gadget: high-speed config #1: c [ 8.820726] android_work: sent uevent USB_STATE=CONFIGURED [ 11.049029] ieee80211_do_open: vif_type=2, p2p=0, ch=3, addr=54:0d:42:ee:1a:22 [ 11.057317] [STA] !!!xradio_vif_setup: id=0, type=2, p2p=0, addr=54:0d:42:ee:1a:22 [ 11.070863] [AP_WRN] BSS_CHANGED_ASSOC but driver is unjoined. [ 11.089296] IPv6: ADDRCONF(NETDEV_UP): wlan0: link is not ready
在使用扫描的时候,会出现如下问题:
root@TinaLinux:/# iw wlan0 scan scan aborted! [ 374.889081] [SCAN_WRN] Timeout waiting for scan complete notification. [ 374.896412] [SCAN_WRN] xradio_scan_timeout:scan timeout cnt=1 [ 374.902934] [SCAN_ERR] Scan failed (-110). [ 375.902951] [WSM_ERR] [FW-DEBUG] DbgId = 4 [ 375.907562] [WSM_ERR] [FW-DEBUG] 0x000F4242
从日志信息来看,通信和下载 WIFI 固件都没有问题,也尝试过使用 wifi -o sta 之后再 wifi -s,也不行。
[ 374.889081] [SCAN_WRN] Timeout waiting for scan complete notification. [ 374.896412] [SCAN_WRN] xradio_scan_timeout:scan timeout cnt=1 [ 374.902934] [SCAN_ERR] Scan failed (-110). [ 375.902951] [WSM_ERR] [FW-DEBUG] DbgId = 4 [ 375.907562] [WSM_ERR] [FW-DEBUG] 0x000F4242 [ 375.912376] [SCAN_WRN] Scan timeout already occured. Don't cancel work [ 730.729141] [SCAN_WRN] Timeout waiting for scan complete notification. [ 730.736667] [SCAN_WRN] xradio_scan_timeout:scan timeout cnt=2 [ 730.743224] [SCAN_ERR] Scan failed (-110). [ 731.743199] [WSM_ERR] [FW-DEBUG] DbgId = 4 [ 731.747803] [WSM_ERR] [FW-DEBUG] 0x000F4241
-
【分析笔记】全志 T507 PF4 引脚无法被正常设置为中断模式的问题分析
相关信息
硬件平台:全志T507
系统版本:Android 10 / Linux 4.9.170
问题描述:PF4 无法通过标准接口设置为中断模式,而 PF1、PF2、PF3、PF5 正常可用。分析过程
一开始以为是引脚被其它驱动占用引起,或者该引脚不具备中断功能,经过排查,已排除这两种可能,因此通过从源码分析来找问题的根因。
以下是以 gpio_keys.c 驱动为入口进行分析:
// drivers/input/keyboard/gpio_keys.c static int gpio_keys_setup_key(struct platform_device *pdev, struct input_dev *input, struct gpio_button_data *bdata, const struct gpio_keys_button *button) { ...... error = devm_request_any_context_irq(&pdev->dev, bdata->irq, isr, irqflags, desc, bdata); } // kernel/irq/devres.c int devm_request_any_context_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) { ...... rc = request_any_context_irq(irq, handler, irqflags, devname, dev_id); if (rc < 0) { devres_free(dr); return rc; } ...... return rc; } // kernel/irq/manage.c int request_any_context_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id) { ...... ret = request_irq(irq, handler, flags, name, dev_id); return !ret ? IRQC_IS_HARDIRQ : ret; } // include/linux/interrupt.h static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) { return request_threaded_irq(irq, handler, NULL, flags, name, dev); } // kernel/irq/manage.c int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) { ...... chip_bus_lock(desc); retval = __setup_irq(irq, desc, action); chip_bus_sync_unlock(desc); ...... return retval; } // kernel/irq/manage.c static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new) { ...... if (!shared) { ret = irq_request_resources(desc); if (ret) { pr_err("Failed to request resources for %s (irq %d) on irqchip %s\n", new->name, irq, desc->irq_data.chip->name); goto out_mask; } ...... } ...... } // kernel/irq/manage.c static int irq_request_resources(struct irq_desc *desc) { struct irq_data *d = &desc->irq_data; struct irq_chip *c = d->chip; return c->irq_request_resources ? c->irq_request_resources(d) : 0; } // drivers/pinctrl/sunxi/pinctrl-sunxi.c static struct irq_chip sunxi_pinctrl_edge_irq_chip = { .name = "sunxi_pio_edge", .irq_ack = sunxi_pinctrl_irq_ack, .irq_mask = sunxi_pinctrl_irq_mask, .irq_unmask = sunxi_pinctrl_irq_unmask, .irq_request_resources = sunxi_pinctrl_irq_request_resources, .irq_release_resources = sunxi_pinctrl_irq_release_resources, .irq_set_type = sunxi_pinctrl_irq_set_type, .irq_set_wake = sunxi_pinctrl_irq_set_wake, }; // drivers/pinctrl/sunxi/pinctrl-sunxi.c static int sunxi_pinctrl_irq_request_resources(struct irq_data *d) { struct sunxi_pinctrl *pctl = irq_data_get_irq_chip_data(d); struct sunxi_desc_function *func; func = sunxi_pinctrl_desc_find_function_by_pin(pctl, pctl->irq_array[d->hwirq], "irq"); if (!func) return -EINVAL; /* Change muxing to INT mode */ printk(KERN_EMERG"[lmx] irq:%d set int mode pin:%d d->hwirq:%ld func->muxval:%d\n", d->irq, pctl->irq_array[d->hwirq], d->hwirq, func->muxval); sunxi_pmx_set(pctl->pctl_dev, pctl->irq_array[d->hwirq], func->muxval); return 0; } // drivers/pinctrl/sunxi/pinctrl-sunxi.c static void sunxi_pmx_set(struct pinctrl_dev *pctldev, unsigned pin, u8 config) { struct sunxi_pinctrl *pctl = pinctrl_dev_get_drvdata(pctldev); unsigned long flags; u32 val, mask; raw_spin_lock_irqsave(&pctl->lock, flags); pin -= pctl->desc->pin_base; val = readl(pctl->membase + sunxi_mux_reg(pin)); mask = MUX_PINS_MASK << sunxi_mux_offset(pin); writel((val & ~mask) | config << sunxi_mux_offset(pin), pctl->membase + sunxi_mux_reg(pin)); raw_spin_unlock_irqrestore(&pctl->lock, flags); }
无论有多复杂的代码,最终都需要通过读写寄存器的方式来实现控制芯片,而通过上述代码分析,即可发现 sunxi_pmx_set() 接口用于配置寄存器,是最底层的接口,可以通过打印输出传入的参数,来检查是否有问题。
PF3 打印输出为:
[ 10.683205] [lmx] irq:148 set int mode pin:163 d->hwirq:131 func->muxval:6
PF4 打印输出为:
[ 10.683557] [lmx] irq:149 set int mode pin:196 d->hwirq:132 func->muxval:6
这里就能看出很奇怪的地方,PF3 的引脚编号是 163,而 PF4 却是 196,跨度很大。
通过以下指令查询 PF4 的正确引脚编号,也可以得知 196 引脚编号是哪一组:
mercury-demo:/ # cat /sys/kernel/debug/pinctrl/pio/pins registered pins: 137 ...... pin 160 (PF0) pin 161 (PF1) pin 162 (PF2) pin 163 (PF3) pin 164 (PF4) pin 165 (PF5) pin 166 (PF6) ...... pin 196 (PG4) pin 197 (PG5) ......
确认 PF4 正确引脚编号是 164,而 196 对应是 PG4,实际生效的是 PG4,通过以下指令即可确认:
mercury-demo:/sys/kernel/debug/sunxi_pinctrl # echo PG4 > sunxi_pin mercury-demo:/sys/kernel/debug/sunxi_pinctrl # cat * pin[PG4] data: 1 pio pin[PG4] dlevel: 1 pin[PG4] funciton: 6 NOMATCH pin[PG4] pull: 1 PG4 pin[PG4] funciton: 6 pin[PG4] data: 1 pin[PG4] dlevel: 1 pin[PG4] pull: 1
根据代码确定引脚编号来源于 pctl->irq_array 数组,通过 pctl->irq_array 赋值的地方进行打印输出,是否一开始就出错了:
// drivers/pinctrl/sunxi/pinctrl-sunxi.c static int sunxi_pinctrl_build_state(struct platform_device *pdev) { ...... /* Count functions associated groups */ for (i = 0; i < pctl->desc->npins; i++) { const struct sunxi_desc_pin *pin = pctl->desc->pins + i; struct sunxi_desc_function *func = pin->functions; while (func->name) { /* Create interrupt mapping while we're at it */ if (!strcmp(func->name, "irq")) { int irqnum = func->irqnum + func->irqbank * IRQ_PER_BANK; pctl->irq_array[irqnum] = pin->pin.number; printk(KERN_EMERG"[lmx] pctl->irq_array[%d] = %d (func->irqnum:%d func->irqbank:%d)\n", irqnum, pin->pin.number, func->irqnum, func->irqbank); } sunxi_pinctrl_add_function(pctl, func->name); func++; } } ...... return 0; } // drivers/pinctrl/sunxi/pinctrl-sunxi.h #define IRQ_PER_BANK 32
可以发现,PF4(164)对应的索引是 132,原本被正确赋值为 164,但又被覆盖为 PG4(196)。
不难发现,出现覆盖的原因是因为 PG4 的 func->irqbank 数值错误(4),导致索引下标计算错误。根据前后文来看,func->irqbank 的正确数值应该是 5,代入计算得到正确的值 164:
int irqnum(164) = func->irqnum(4) + func->irqbank(5) * IRQ_PER_BANK(32);大概率硬件资源描述配置出错,通过搜索 irqbank 被赋值的方法,来定位描述配置出错的地方:
// drivers/pinctrl/sunxi/pinctrl-sunxi.h #define SUNXI_FUNCTION_IRQ_BANK(_val, _bank, _irq) \ { \ .name = "irq", \ .muxval = _val, \ .irqbank = _bank, \ .irqnum = _irq, \ }
使用的是 SUNXI_FUNCTION_IRQ_BANK 宏,重点检查第二个参数:
// drivers/pinctrl/sunxi/pinctrl-sun50iw9p1.c static const struct sunxi_desc_pin sun50iw9p1_pins[] = { ...... SUNXI_PIN(SUNXI_PINCTRL_PIN(G, 3), SUNXI_FUNCTION(0x0, "gpio_in"), SUNXI_FUNCTION(0x1, "gpio_out"), SUNXI_FUNCTION(0x2, "sdc1"), /* D1 */ SUNXI_FUNCTION_IRQ_BANK(0x6, 5, 3), /* PG_EINT3 */ SUNXI_FUNCTION(0x7, "io_disabled")), SUNXI_PIN(SUNXI_PINCTRL_PIN(G, 4), SUNXI_FUNCTION(0x0, "gpio_in"), SUNXI_FUNCTION(0x1, "gpio_out"), SUNXI_FUNCTION(0x2, "sdc1"), /* D2 */ // 可以发现第二个参数恰好是 4,根据分析结果,以及结合上下文,正确的应该是 5 SUNXI_FUNCTION_IRQ_BANK(0x6, 4, 4), /* PG_EINT4 */ SUNXI_FUNCTION(0x7, "io_disabled")), SUNXI_PIN(SUNXI_PINCTRL_PIN(G, 5), SUNXI_FUNCTION(0x0, "gpio_in"), SUNXI_FUNCTION(0x1, "gpio_out"), SUNXI_FUNCTION(0x2, "sdc1"), /* D3 */ SUNXI_FUNCTION_IRQ_BANK(0x6, 5, 5), /* PG_EINT5 */ SUNXI_FUNCTION(0x7, "io_disabled")), ...... };
修改之后的 pctl->irq_array 打印输出正确:
进行实测,PF4 已经可以正常的被设置为中断模式。
问题总结
全志原厂提供的 SoCs pinctrl driver 中的 PG4 中断信息描述错误,导致覆盖了 PF4 的引脚编号,因此只要修正 PG4 的描述信息,即可解决问题。
这个问题不仅仅会影响 PF4 无法使用,也会影响 PG4 引脚无法使用,从代码来看,想要设置为 PG4 为中断模式,实际修改的会 PA0(0)。
--- a/longan/kernel/linux-4.9/drivers/pinctrl/sunxi/pinctrl-sun50iw9p1.c +++ b/longan/kernel/linux-4.9/drivers/pinctrl/sunxi/pinctrl-sun50iw9p1.c @@ -693,7 +693,7 @@ SUNXI_FUNCTION(0x0, "gpio_in"), SUNXI_FUNCTION(0x1, "gpio_out"), SUNXI_FUNCTION(0x2, "sdc1"), /* D2 */ - SUNXI_FUNCTION_IRQ_BANK(0x6, 4, 4), /* PG_EINT4 */ + SUNXI_FUNCTION_IRQ_BANK(0x6, 5, 4), /* PG_EINT4 */ SUNXI_FUNCTION(0x7, "io_disabled")), SUNXI_PIN(SUNXI_PINCTRL_PIN(G, 5), SUNXI_FUNCTION(0x0, "gpio_in"),
-
【随笔记】C++ condition_variable 陷阱
问题说明
通过 std::condition_variable 来实现超时等待,会受到系统时间变化的影响,系统时间倒退修改就会导致延后唤醒,系统时间提前将会导致提前被唤醒,返回结果仍为超时。
这种问题只有在系统时间发生变化的时候才会出现,例如搭配 NTP 更新功能,硬件还未同步时间时,一般在 1993 年,此时使用了 wait_for() 这类接口等待 10 秒,结果在 10 秒内被 ntp 同步更新了时间到 2023,那么时间生效的一瞬间,wait_for() 就会直接被唤醒,且返回的结果是超时唤醒。
另外一种时间倒退的场景,则影响会更大,例如在 2023 年,时间调回了 2022 年,那么 wait_for() 将会等待一年多才会被超时唤醒,代码执行的现象就像是调用了 wait() 的效果。
通过分析 std::condition_variable 源码,可以很清晰看到使用的是系统时间:
示例代码:
实现一个可以随时被打断的延时等待类。
有隐患的代码:
bool DelayControl::delay(unsigned int millisecond) { bool is_timeout = false; unique_lock< mutex > lock(mutex_data_); is_runing_ = true; is_timeout = (cv_status::timeout == cond_.wait_for(lock, chrono::milliseconds(millisecond))); is_runing_ = false; lock.unlock(); return is_timeout; } void DelayControl::stop() { unique_lock< mutex > lock(mutex_data_); cond_.notify_all(); }
改进方案一(使用 select 方式实现):缺点是一个对象会浪费两个文件描述符资源
DelayControl::DelayControl() { is_runing_ = false; pipe(pipefd_); } bool DelayControl::delay(unsigned int millisecond) { int result; fd_set rdfs; struct timeval timeout; bool is_timeout = false; is_runing_ = true; FD_ZERO(&rdfs); FD_SET(pipefd_[0], &rdfs); timeout.tv_sec = millisecond / 1000; timeout.tv_usec = (millisecond - ((millisecond / 1000) * 1000)) * 1000; switch((result = select(pipefd_[1] + 1, &rdfs, NULL, NULL, &timeout))){ case 0: is_timeout = true; break; } is_runing_ = false; return is_timeout; } void DelayControl::stop() { write(pipefd_[1], "", 1); }
改进方案二(使用 pthread_cond_timedwait 方式实现):完美方案
关键在于使用了 CLOCK_MONTONIC ,其用不是系统时间,而是内核的计数器 jiffies,系统每次启动时,jiffies初始化为0。每来一个timer interrupt,jiffies加1,即它代表系统启动后流逝的tick数,jiffies 只会单调递增。
DelayControl::DelayControl() { is_runing_ = false; pthread_condattr_init(&cond_cattr_); pthread_mutex_init(&mutex_data_, NULL); pthread_condattr_setclock(&cond_cattr_, CLOCK_MONOTONIC); pthread_cond_init(&cond_, &cond_cattr_); } DelayControl::~DelayControl() { pthread_mutex_lock(&mutex_data_); pthread_cond_broadcast(&cond_); pthread_mutex_unlock(&mutex_data_); pthread_cond_destroy(&cond_); pthread_mutex_destroy(&mutex_data_); } bool DelayControl::delay(unsigned int millisecond) { struct timespec tv; bool is_timeout = false; pthread_mutex_lock(&mutex_data_); is_runing_ = true; clock_gettime(CLOCK_MONOTONIC, &tv); millisecond += (tv.tv_sec * 1000) + (tv.tv_nsec / 1000000); tv.tv_sec = millisecond / 1000; tv.tv_nsec = (millisecond - ((millisecond / 1000) * 1000)) * 1000 * 1000; is_timeout = pthread_cond_timedwait(&cond_, &mutex_data_, &tv) ? true : false; is_runing_ = false; pthread_mutex_unlock(&mutex_data_); return is_timeout; } bool DelayControl::isRuning() { bool is_runing = false; pthread_mutex_lock(&mutex_data_); is_runing = is_runing_; pthread_mutex_unlock(&mutex_data_); return is_runing; } void DelayControl::stop() { pthread_mutex_lock(&mutex_data_); pthread_cond_broadcast(&cond_); pthread_mutex_unlock(&mutex_data_); }
用如下随机设置系统时间的方式压力测 6 小时通过:
#define RAND(_MIN_, _MAX_) (rand() % (_MAX_-_MIN_+1) + _MIN_) int main() { Logger::getInstance().init("/mnt/UDISK/pre_bullying/logs/DelayControl.log", 1024*1024*2, 1); std::shared_ptr<MeasureTime> sp_timer_; std::shared_ptr<DelayControl> sp_delay_; sp_delay_ = std::make_shared<DelayControl>(); sp_timer_ = std::make_shared<MeasureTime>(100); srand((unsigned)time(NULL)); { DelayControl mDelayControl; mDelayControl.delay(1000); } std::thread t([&]{ char buf[64] = {0}; while(true){ usleep(RAND(0, 5000) * 1000); system("ntpclient -s -c 1 -h ntp7.aliyun.com -i 3"); usleep(RAND(0, 5000) * 1000); snprintf(buf, sizeof(buf), "date -s \"%.4d-%.2d-%.2d %.2d:%.2d:%.2d\"", RAND(1990, 2030), RAND(1, 12), RAND(1, 29), RAND(0, 23), RAND(1, 60), RAND(1, 60)); iprint("set time:[%s]", buf); system(buf); } }); t.detach(); while(true) { int delay = RAND(0, 5000); unsigned long long ms = 0; iprint("delay:-->[%d]", delay); sp_timer_->update(); bool isdone = sp_delay_->delay(delay); ms = sp_timer_->getMillisecond(); iprint("delay %s:[%d][%d][%lld]", delay != ms ? "delay != ms" : "done", isdone, delay, sp_timer_->getMillisecond()); } return 0; }
-
【分析笔记】Linux tasklet 机制的理解
Tasklet 介绍
Linux 内核提供的四种中断下半部中 softirq(软中断)、tasklet(小任务)、workqueue(工作队列) 、request thread(中断线程)中的其中一种,其效率仅次于软中断,但远高于request thread 和 workqueue。
-
软中断(softirq) 之所以性能高的原因,在 SMP 系统下多个 cpu 同时并发处理
如网卡的 fifo 半满中断触发,被 cpu0 处理,cpu0 会在关闭中断后,将数据从网卡的 fifo 拷贝到 ram 之后触发软中断,再打开中断,基于谁触发谁处理原则,cpu0 会继续执行软中断服务函数。若网卡的 fifo 全满中断有再次触发,就会被 cpu1 处理,同样是关闭中断后拷贝数据再开启中断,再去触发和执行软中断进行网卡数据包处理。若此时 cpu0\cpu1 都还在软中断处理数据,网卡再次产生中断,那么 cpu2 就会继续相同的流程。由此可见,软中断充分利用的多 cpu 进行并发处理,因此性能非常高,但也同时因为并发的存在,就需要考虑临界区的问题。 -
小任务(tasklet) 之所以性能较软中断差,是因为同一种小任务在多个 cpu 上不会并发执行
由于 tasklet 基于 softirq 的基础实现,为了易用性考虑,同一种 tasklet 在多个 cpu 上不会并行执行,因此不存在并发问题,在使用上就可以少一些顾虑。但也正是因为不存在并发,导致了性能较之 softirq 差一些。 -
tasklet 之所以比 workqueue 和 request thread 性能高
原因是因为前者是在软中断上下文件工作(意味着不能调用任何阻塞的接口),而后两者是在进程上下文工作(实质上是在内核线程里面执行)。
使用示例模版
#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/interrupt.h> static struct tasklet_struct my_tasklet; static void my_tasklet_handle(unsigned long data) { printk("tasklet handle running...\n"); } static irqreturn_t xxx_interrupt(int irq, void *dev_id) { // 调度 tasklet tasklet_schedule(&my_tasklet); } static int __init demo_driver_init(void) { // 初始化一个 tasklet ,关联处理函数 tasklet_init(&my_tasklet, my_tasklet_handle, 0); request_irq(xxx, xxx_interrupt, IRQF_SHARED, xxx, xxx); return 0; } static void __exit demo_driver_exit(void) { tasklet_kill(&my_tasklet); return ; } module_init(demo_driver_init); module_exit(demo_driver_exit); MODULE_LICENSE("GPL v2");
内核源码分析
Linux 内核被启动后,会执行 start_kernel() 函数,tasklet 是基于 softirq 实现的,会在 softirq_init() 里面进行必要的初始化,主要是初始化 tasklet 链表和与相应的软中断号建立关联。
tasklet_hi_action 是高优先级的 tasklet,tasklet_action 是普通的 tasklet,两者实现原理都一样。
// kernel\linux-4.9\init\main.c asmlinkage __visible void __init start_kernel(void) { ... softirq_init(); ... } // kernel\linux-4.9\kernel\softirq.c void __init softirq_init(void) { int cpu; // 初始化链表 for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } // 建立TASKLET_SOFTIRQ、HI_SOFTIRQ软中断号的对应服务接口 open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); }
当调用 tasklet_schedule() 时,如果该 tasklet 没有被设置 TASKLET_STATE_SCHED 标记时,才会加入链表内,如果已经设置了 TASKLET_STATE_SCHED 了,那么就会忽略此次的 tasklet _schedule(),这就意味着如果在极短的时间内调用 tasklet_schedule() 只会触发一次(这里可能会存在丢失中断事件的情况),之后会通过 raise_softirq_irqoff() 启用 TASKLET_SOFTIRQ 软中断。
哪颗 cpu 受理该软中断,就将 tasklet 加入到该 cpu 的 tasklet 链表内,由于相同的软中断可以同时被其他 cpu 触发执行,因此会出现 cpu0\cpu1 的 tasklet 链表内有同一个 tasklet 的情况。
// kernel\linux-4.9\include\interrupt.h static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); } // kernel\linux-4.9\kernel\softirq.c void __tasklet_schedule(struct tasklet_struct *t) { unsigned long flags; // 将指定的 tasklet 加入到链表内并设置软中断 local_irq_save(flags); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_restore(flags); }
在初始化的时候已经为 TASKLET_SOFTIRQ 软中断与 tasklet_action() 建立关联的关系,因此软中断触发时,就会调用 tasklet_action(),这里是精髓部分。
多次触发软中断时,当前 cpu 只能同一时间执行一次相同的软中断,但是如果有多个 cpu 的话,那么会有多个 cpu 并发执行软中断,所以下面的 tasklet_action() 要想到这一点。
-
tasklet_action() 屏蔽当前 CPU 中断,获取当前 CPU 的 tasklet 链表并清空原有的链表,在恢复中断。避免在操作链表的过程中,被硬件中断打断。
-
开始遍历链表取出 tasklet,先 TASKLET_STATE_RUN 标记确定该 tasklet 是否已经被其它 cpu 执行,因为该 tasklet 在执行的过程中,又被加入到当前的 cpu 的 tasklet 链表内。
-
如果没有被执行,就继续检查该 tasklet 是否被 tasklet_disable(),如果有被 disable 就清除 TASKLET_STATE_RUN 标记,这样可以重新被添加会当前 tasklet 链表内,等待再次执行。如果有被执行,即使又被设置了 TASKLET_STATE_RUN 标记,也会在第 5 步执行完成后,会清除掉该标记。
-
如果没有被 disable,那么就清除 TASKLET_STATE_SCHED 标记,该标记一旦被清除,就意味着在 tasklet 执行期间,tasklet_schedule() 可以继续添加新的 tasklet 其他 cpu 的 tasklet 链表内。如果当前 cpu 已经完成了 tasklet_action() ,新的 tasklet 也可能会重新添加到当前的 tasklet 链表。
-
这里就会执行通过 tasklet_init() 绑定的 func,也就是示例中的 my_tasklet_handle(),执行完成后再清除 TASKLET_STATE_RUN 标记,继续下一个 tasklet。
-
能走到这一步,会有两种情况,一种情况是即将执行 tasklet ,发现已经被 disable 掉了,另外一种情况是 tasklet 已经在其它 CPU 上执行中。无论哪种情况,都会将当前的 tasklet 重新放回到当前 cpu 的 tasklet 链表内,并调用 __raise_softirq_irqoff() 重新触发软中断(应该是启用该软中断)。
注意,以上获取 TASKLET_STATE_RUN 和 TASKLET_STATE_SCHED 标记都是位原子操作,所以不会出现因并发引发的问题。
// kernel\linux-4.9\kernel\softirq.c // 某 CPU 要调度各个 tasklet 的实现 static __latent_entropy void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list; // 1 ------------------------------------------------------ local_irq_disable(); list = __this_cpu_read(tasklet_vec.head); // 获取 tasklet 链表 __this_cpu_write(tasklet_vec.head, NULL); // 清空 tasklet 链表 __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head)); local_irq_enable(); // 如果存在 tasklet 就会进入循环 while (list) { struct tasklet_struct *t = list; list = list->next; // 2 ------------------------------------------------------ // TASKLET_STATE_SCHED: 表示该 tasklet 已经被挂接到某个 CPU 上 // TASKLET_STATE_RUN: 表示该 tasklet 正在某个 CPU 上执行 // 检查并设置 TASKLET_STATE_RUN 标记 // 返回 1: 表示 tasklet 没有被执行 返回 0: 表示 tasklet 已经被执行 if (tasklet_trylock(t)) { // 3 ------------------------------------------------------ // 如果当前 tasklet 没有被 tasklet_disable() if (!atomic_read(&t->count)) { // 4 ---------------------------------------------- // 清除 TASKLET_STATE_SCHED 状态,便于该 tasklet 可以再次被触发 if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); // 5 ---------------------------------------------- // 这期间,该 tasklet 可以被 tasklet_schedule(),从而引出下面第二种情况 t->func(t->data); // 如果没有执行且没有 disable 则执行 tasklet_unlock(t); // 清理 TASKLET_STATE_RUN 标记 continue; // 继续下一个 tasklet } // 如果当前已经被 disable 了,那就清理 TASKLET_STATE_RUN 标记 tasklet_unlock(t); } // 6 ----------------------------------------------------- // 有两种情况下,会将该 tasklet 再挂接回链表内,并重新触发,等待下一次执行的机会 // 1. 如果没有被执行,但是被调用 tasklet_disable() 接口 disable 了 // 2. 当前 tasklet 已经在其它 CPU 正在执行 func 这时候 tasklet 又会被挂回在 // 原来的链表中,为了满足同一种类型的 tasklet 只能在一个 CPU 上执行的设计 // 因此此次不执行 tasklet,挂入链表后等待下一次被执行的时机执行 local_irq_disable(); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); __raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_enable(); } }
贴出判断和标记和清除 tasklet 运行的 TASKLET_STATE_RUN 标记代码
// kernel\linux-4.9\include\interrupt.h static inline int tasklet_trylock(struct tasklet_struct *t) { return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state); } // kernel\linux-4.9\include\interrupt.h static inline void tasklet_unlock(struct tasklet_struct *t) { smp_mb__before_atomic(); clear_bit(TASKLET_STATE_RUN, &(t)->state); }
贴出关闭 tasklet 执行的代码
// kernel\linux-4.9\include\interrupt.h static inline void tasklet_disable(struct tasklet_struct *t) { tasklet_disable_nosync(t); tasklet_unlock_wait(t); smp_mb(); } // kernel\linux-4.9\include\interrupt.h // 关闭 tasklet(实际上应该理解为暂停调度执行) static inline void tasklet_disable_nosync(struct tasklet_struct *t) { // 这里对整型原子操作 count 自增了,对应 tasklet_action() 里面的 atomic_read() atomic_inc(&t->count); smp_mb__after_atomic(); } // kernel\linux-4.9\include\interrupt.h static inline void tasklet_enable(struct tasklet_struct *t) { // 这里对整型原子操作 count 自减了,对应 tasklet_action() 里面的 atomic_read() smp_mb__before_atomic(); atomic_dec(&t->count); } tasklet_init() 实现与用户函数关联的接口,也是很简单 // kernel\linux-4.9\kernel\softirq.c void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->func = func; t->data = data; }
tasklet_kill() 主要实现是尽可能快的让 tasklet 得到执行,等待执行完成后再退出。
-
判断该 tasklet 是否已经被挂接到某个 cpu 的 tasklet 链表内,如果有挂接到,那么就立即让出 cpu,直至 tasklet 清除 TASKLET_STATE_SCHED 标记(参考 tasklet_action() 第 4 个步骤),进入运行状态。
-
如果 tasklet 没有挂接或者一旦进入到执行状态,那么就会不停的检测 TASKLET_STATE_RUN 是否有被清除,被清除的话说明已经运行完成(参考 tasklet_action() 第5个步骤),可以放心的退出了。
// kernel\linux-4.9\kernel\softirq.c void tasklet_kill(struct tasklet_struct *t) { if (in_interrupt()) pr_notice("Attempt to kill tasklet from interrupt\n"); // 1 ---------------------------------------------- while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { do { yield(); } while (test_bit(TASKLET_STATE_SCHED, &t->state)); } // 2 ---------------------------------------------- tasklet_unlock_wait(t); clear_bit(TASKLET_STATE_SCHED, &t->state); } // kernel\linux-4.9\include\interrupt.h static inline void tasklet_unlock_wait(struct tasklet_struct *t) { while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { barrier(); } }
至此,tasklet 关键的实现原理分析完成,实际上还应联合 softirq,才算完整的了解整个机制。
Tasklet 机制总结
-
每颗 cpu 都有自己的 tasklet 链表,这样可以将 tasklet 分布在各个 cpu 上,可实现并发不同的 tasklet。
-
相同的 tasklet 只能在某一颗 cpu 上串行执行,其它 cpu 会暂时避让,在此情况下,不需要考虑并发问题(即不需要加锁)。
3. tasklet_schedule() 接口调用时,如果 tasklet 还未被执行,或者处于 disable 期间,指定的 tasklet 不会被加入链表内,即该请求不会被受理。
- tasklet_disable() 接口只是暂时停止指定的 tasklet 执行,依然会被加回待执行链表内。而在 disable 期间,相同的 tasklet 将无法被加入链表调度。
-
-
【分析笔记】Linux 内核自旋锁的理解和使用原则
自旋锁简单说明自旋锁主要解决在竞态并发下,保护执行时间很短的临界区。它只允许一个执行单位进入临界区,在该执行单位离开前,其它的执行单位将会在进入临界区前不停的循环等待(即所谓的自旋),直至该执行单位离开临界区后,最先等待的一个执行单位会立即进入临界区。此方式不涉及到上下文切换,因此效率极高。
出现并发的场景
硬中断触发打断当前进程、softirq、tasklet、timer等形成的并发
softirq(软中断)、tasklet(小任务)、timer(内核定时器) 触发打断 当前进程(或内核线程)形成的并发
在 SMP 系统下,多次触发 softirq 之间形成的并发(同一个 softirq 可在多个 cpu 并发执行)
在 SMP 系统下,不同 tasklet、timer 之间的并发(同一个 tasklet 和 timer 不会并发执行)
在内核抢占的调度机制形成高低优先级进程之间(或内核线程)的并发额外的注意事项
一、软中断在同一个cpu下并不会并发,但是在多个cpu下是可以并发的,因此性能很高。
如网卡接受数据,产生一个中断后,被 cpu0 处理,关闭中断后,将数据从网卡的 fifo 拷贝到 ram 之后触发软中断,再打开中断,基于谁触发谁处理原则,cpu0 会继续执行软中断服务函数。此时网卡又再次产生中断,会被 cpu1 处理,同样是关闭中断后拷贝数据再开启中断,再去触发和执行软中断进行网卡数据包处理。若此时 cpu0\cpu1 都还在软中断处理数据,网卡再次产生中断,那么 cpu2 就会继续参与,由此可见,软中断充分利用的多 cpu 进行并发处理,因此性能非常高,但也同时因为并发的存在,就需要考虑临界区的问题。
二、同一个 tasklet、timer 在同一时间,只会在一个cpu上运行,是为了易用性做出的牺牲。
由于 tasklet,timer 都是基于 softirq 的基础实现,为了易用性考虑,与 softirq 不同的是,同一种tasklet、timer 在多个cpu上也不会并行执行,因此不存在并发问题。
三、新版本的 Linux 内核不再支持中断嵌套(不确定是从哪个版本开始,以下为内核补丁说明)
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc自旋锁的种类说明
最基础的自旋锁有三个版本:
- spin_lock()\spin_unlock()
这是最基础的自旋锁,也是对系统影响最小的自旋锁,在未获得锁时,会自旋等待进入临界区。
- spin_lock_bh()\spin_unlock_bh()
这是在最基础的自旋锁上获取锁之前,先关闭中断底半部,明确的来说就是关闭软中断(包含基于软中断实现的 tasklet 和 timer),主要影响系统的软中断类的并发。
- spin_lock_irq()\spin_unlock_irq()、spin_lock_irqsave()\spin_unlock_irqrestore()
这是在最基础的自旋锁上获取锁之前,先屏蔽当前 cpu 的中断,禁止内核抢占当前进程,主要用于防止软硬件中断并发,影响最大,它影响了当前 CPU 的软硬中断和进程调度。
spin_lock_irq() 是会屏蔽当前 cpu 所有的中断,spin_unlock_irq() 会开启当前 cpu 所有的中断。spin_lock_irqsave() 是现将当前 cpu 的中断使能位取出来,然后在屏蔽当前 cpu 所有中断,spin_unlock_irqrestore() 再恢复之前的中断使能位。
凡是用到 spin_lock_irq()\spin_unlock_irq() 都可以用 spin_lock_irqsave()\spin_unlock_irqrestore() 替换,根据使用情况决定选择哪种方式即可。例如希望中断执行完成后,所有的中断都要开启,那就选择 spin_lock_irq()\spin_unlock_irq(),如果希望中断执行完成后,只需要恢复执行前的中断开关状态,那么就选择 spin_lock_irqsave()\spin_unlock_irqrestore(),如执行前 A中断 本来就要求关闭的,那么执行完之后,还是希望 A中断 仍处于关闭状态。
使用自旋锁的原则
首先要先明确硬件中断的优先级最高,它可以随时打断软中断和内核线程与用户进程,他们之间的优先级如下:
硬中断 >>> 软中断(含基于软中断实现的 tasklet、timer) >>> 内核线程\用户进程
然后需要确定谁可能会并发访问临界区,然后遵循如下规则,选择合适的锁即可:
- 低优先级要防着高优先级的,用能禁止高优先级的自旋锁,而高优先级的只需最简单的锁
- 同等级要防着同等级的, 就使用最简单自旋锁
一、低优先级要防着高优先级的,用能禁止高优先级的自旋锁,而高优先级的只需最简单的锁
例1:用户进程上下文或内核线程 和 硬件中断 都会访问同一个临界区
用户进程:使用 spin_lock_irq()\spin_unlock_irq()
硬件中断:使用 spin_lock()\spin_unlock()
进程上下文访问临界区要防止被硬件中断打断侵入,就需要通过调用 spin_lock_irq()\spin_unlock_irq() 禁止当前 CPU 的中断再去获取锁,那么临界区内就不会被硬件中断访问。但它也只能关闭当前 cpu 的中断,此时其它 cpu 还能继续响应中断,所以中断内部还是需要加上 spin_lock()\spin_unlock() 来保护临界区,即使该中断未拿到锁而持续自旋,也不会影响进程上下文继续执行,顶多就自旋等待一会就能获得锁。
这里也能说明,被自旋锁保护的临界区代码不能太过复杂,不然在这种场景下,就会导致中断自旋时间过长,在该中断自旋期间就无法响应其它的中断,如 tick 心跳中断,最终可能导致系统异常死机。
例2:软中断(softirq、tasklet、timer) 和 硬件中断 都会访问同一个临界区
软件中断:使用 spin_lock_irq()\spin_unlock_irq()
硬件中断:使用 spin_lock()\spin_unlock()
例3:用户进程上下文或内核线程 和 软中断(softirq、tasklet、timer) 都会访问同一个临界区
用户进程:使用 spin_lock_bh()\spin_unlock_bh()
软件中断:使用 spin_lock()\spin_unlock()
进程上下文访问临界区要防止被软中断打断侵入,就需要使用 spin_lock_bh()\spin_unlock_bh() 禁用软中断,但只能关闭当前 cpu 的软中断,其它 cpu 依然能响应软中断,因此还需在软中断中使用 spin_lock()\spin_unlock() 来保护临界区。
二、同等级要防着同等级的, 就使用最简单自旋锁
例1:用户进程上下文或内核线程 和 用户进程上下文和内核线程 即多个进程会访问同一个临界区
只需要使用:spin_lock()\spin_unlock(),因为内核支持抢占调度,所以需要上锁。
例2:不同的 硬件中断 都会访问同一个临界区
只需要使用: spin_lock()\spin_unlock(),不同的硬件中断是可以同时被多颗 cpu 响应处理的,因此需要使用自旋锁进行保护。
如果是旧版内核支持中断嵌套的,则应该使用 spin_lock_irq()\spin_unlock_irq(),以避免被高优先级中断抢占,从而导致出现死锁情况。
例3:不同的 tasklet、timer 会访问同一个临界区
只需要使用:spin_lock()\spin_unlock(),因为不同的 tasklet 或 timer 是可以在不同的 cpu 并发执行。
注意,如果只有相同的 tasklet 或者 timer 访问临界区,是不需要加锁的,因为相同的 tasklet 或 timer 不会并发,即使是有多个 cpu 也不会。
例4:在一个或者多个软中断(softirq) 中会访问同一个临界区
只需要使用:spin_lock()\spin_unlock(),虽然同一时间一个 cpu 只能执行一个软中断,但其它的 cpu 还是可以并发执行相同的软中断的。
三、workqueue、waitqueue、completion 用锁规则
workqueue(工作队列)是基于内核线程实现、waitqueue(等待队列)工作在用户进程上下文、completion(完成量)是基于等待队列实现也是工作在用户进程上下文,因此它们的用锁规则等同于用户进程。
自旋锁的代码分析
自旋锁在不同的硬件环境的实现不一样,此处分析以最复杂的环境下自旋锁的实现原理,即:SMP 下支持任务抢占的硬件环境。
一、自旋锁初始化
锁的数据结构定义,这个数据结构用的是结构体内嵌共用体设计,理解这点非常重要。
#define TICKET_SHIFT 16 // 指明 owner 和 next 的位宽 typedef struct { union { u32 slock; // 32 位 struct __raw_tickets { u16 owner; // 16 位 解锁计数 u16 next; // 16 位 上锁计数 } tickets; }; } arch_spinlock_t; 锁的初始化 spin_lock_init() :lock->raw_lock 成员变量被设置为 __ARCH_SPIN_LOCK_UNLOCKED { { 0 } },即 lock->tickets.owner = 0, lock->tickets.next = 0 #define spin_lock_init(_lock) \ do { \ spinlock_check(_lock); \ raw_spin_lock_init(&(_lock)->rlock); \ } while (0) # define raw_spin_lock_init(lock) \ do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0) #define __RAW_SPIN_LOCK_UNLOCKED(lockname) \ (raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname) #define __RAW_SPIN_LOCK_INITIALIZER(lockname) \ { \ .raw_lock = __ARCH_SPIN_LOCK_UNLOCKED, \ SPIN_DEBUG_INIT(lockname) \ SPIN_DEP_MAP_INIT(lockname) } #define __ARCH_SPIN_LOCK_UNLOCKED { { 0 } }
二、自旋锁上锁的分析
static __always_inline void spin_lock(spinlock_t *lock) { raw_spin_lock(&lock->rlock); // 调用宏 } #define raw_spin_lock(lock) _raw_spin_lock(lock) // 调用 _raw_spin_lock void __lockfunc _raw_spin_lock(raw_spinlock_t *lock) { __raw_spin_lock(lock); // 继续调用 __raw_spin_lock } static inline void __raw_spin_lock(raw_spinlock_t *lock) { // 设置当前进程不可被抢占 preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); // 调用 do_raw_spin_lock LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
上面的代码使用 preempt_disable() 设置当前进程不可被抢占,此举可避免在持锁期间被高优先级进程抢占当前进程去访问临界区。
void do_raw_spin_lock(raw_spinlock_t *lock) { debug_spin_lock_before(lock); arch_spin_lock(&lock->raw_lock); // 调用 arch_spin_lock debug_spin_lock_after(lock); } static inline void arch_spin_lock(arch_spinlock_t *lock) { unsigned long tmp; u32 newval; arch_spinlock_t lockval; prefetchw(&lock->slock); __asm__ __volatile__( "1: ldrex %0, [%3]\n" // 4 ------------------------- " add %1, %0, %4\n" // 5 ------------------------- " strex %2, %1, [%3]\n" // 6 ------------------------- " teq %2, #0\n" // 7 ------------------------- " bne 1b" : "=&r" (lockval), "=&r" (newval), "=&r" (tmp) // 1 ----------- : "r" (&lock->slock), "I" (1 << TICKET_SHIFT) // 2 ------------ : "cc"); // 3 指明上述汇编指令会改变条件寄存器 // 8 --------------------------------------------- while (lockval.tickets.next != lockval.tickets.owner) { wfe(); // 让当前 cpu 进入低功耗模式 lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); } // 9 --------------------------------------------- smp_mb(); }
这部分的代码是实现自旋锁的核心,是通过内嵌汇编指令实现,使用比较关键的汇编指令 ldrex\strex 实现原子访问:
-
ldrex Rx, [Ry]:读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问
-
strex Rx, Ry, [Rz]:如果发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。
-
ldrex 负责拷贝数据和独占访问标记,strex 在根据标记存在与否拷贝数据和清除标记的过程是原子操作。
以下为 arch_spin_lock() 的代码进行逐行解释:
-
将 lockval、newval、tmp 局部变量分别与 %0、%1、%2 编号关联(编号对应由编译器指定 CPU 的寄存器)
-
将 &lock->slock 、(1 << TICKET_SHIFT) 分别与 %3、%4 编号关联(编号会由编译器指定 CPU 的寄存器),这里的 TICKET_SHIFT 含义是指明内部变量位宽,代码定义的是 16,表面是 16 位宽的变量,也就是说 %4 是与 1 << 16 关联,便于汇编指令计算。
-
指定此内嵌的汇编指令会修改条件寄存器
-
ldrex 指令实现读取 %3(lock->slock) 里面的数据到 %0(lockval),也就是将入参 lock 的数据拷贝到局部变量 lockval 中,并标记 lock 所在内存的独占访问标记。这一步主要是记录当前锁的计数。
-
add 指令实现将 %0(lockval) + %4(1 << 16) ,结果放到 %1(newval),实现的效果等同于 newval= lockval.slock + (1 << 16)。这个步骤是根据 arch_spinlock_t 数据结构设计的,它内部是一个共同体,slock 与 tickets 使用的是相同的内存空间,slock 的低 16 位等同于 tickets.owner,高 16 位宽等同于 tickets.next。
-
strex 会先检查 %3(lock->slock) 这块内存的独占标记是否还在,如果不在则设置 %2(tmp) 为 1,说明已经被其它线程修改了,如果还在的话设置为 0,将 %1(newval) 数据覆盖到 %3(lock->slock) ,再将独占访问标记清除。这两步主要是借助中间变量 newval 对 next 计数进行自增后,更新到原 lock 里面。
-
teq 和 bne 指令实现判断如果 %2(tmp) 不等于 0,说明已经被其它线程修改了,重新再跳转到标签 1 执行,也就是重新跳转回第 4 个步骤继续执行,否则就继续往下执行。
-
这里主要是不停的判断当前的当前锁的 owen 是否与当前的 next 相等,如果不相等则一直循环检查,这个步骤就是实现了我们所说的自旋功能。当其它持有锁的线程要对该锁进行解锁,解锁的操作会将 owen 自增,当 owen 与当前记录的 next 相等,就会让当前线程退出自旋。
-
自旋退出了,就意味着拿到锁了,就可以访问临界区了。
三、自旋锁解锁的分析
static __always_inline void spin_unlock(spinlock_t *lock) { raw_spin_unlock(&lock->rlock); } #define raw_spin_unlock(lock) _raw_spin_unlock(lock) void __lockfunc _raw_spin_unlock(raw_spinlock_t *lock) { __raw_spin_unlock(lock); } static inline void __raw_spin_unlock(raw_spinlock_t *lock) { spin_release(&lock->dep_map, 1, _RET_IP_); do_raw_spin_unlock(lock); // 2 ------------------------------------- preempt_enable(); } void do_raw_spin_unlock(raw_spinlock_t *lock) { debug_spin_unlock(lock); arch_spin_unlock(&lock->raw_lock); } static inline void arch_spin_unlock(arch_spinlock_t *lock) { smp_mb(); // 1 ------------------------------------- lock->tickets.owner++; dsb_sev(); }
-
对 lock->tickets.owner 进行自增,这样可以让等待锁的线程退出自旋
-
恢复内核对当前线程的抢占
推演自旋锁的工作过程:
从自旋锁刚进行初始化的状态来推演:lock->tickets.owner = 0,lock->tickets.next = 0
- 线程A:开始申请锁:读取锁 lock 到临时变量 A.lockval 并设置该内存区域独占标记,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 0,A.lockval.tickets.owner = 0,A.lockval.tickets.next = 0
- 线程B:开始申请锁:读取锁 lock 到临时变量 B.lockval 并设置该内存区域独占标记,此时的状态:
lock->tickets.owner =0,lock->tickets.next = 0,B.lockval.tickets.owner = 0,B.lockval.tickets.next = 0
- 线程A:计算锁序号:对 A.newval = A.lockval.slock + (1<<16) = 0x10000,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 0,A.lockval.tickets.owner = 0,A.lockval.tickets.next = 0,A.newval = 0x10000
4. 线程C:开始申请锁:读取锁 lock 到临时变量 lockvalC 并设置该内存区域独占标记,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 0,C.lockval.tickets.owner = 0,C.lockval.tickets.next = 0
5. 线程B:计算锁序号:对 B.newval = B.lockval.slock + (1<<16) = 0x00 + 0x10000,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 0,B.lockval.tickets.owner = 0,B.lockval.tickets.next = 0,B.newval = 0x10000
- 线程A:更新锁计数:检查该内存区域的独占访问标记存在,将 A.newval 覆盖到 lock->slock 并清除独占标记,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 1,A.lockval.tickets.owner = 0,A.lockval.tickets.next = 0,A.newval = 0x10000
- 线程A:锁持有检测:由于 A.lockval.tickets.next == A.lockval.tickets.owner 相等则跳出 while 循环,正式持有锁返回了。
lock->tickets.owner = 0,lock->tickets.next = 1,A.lockval.tickets.owner = 0,A.lockval.tickets.next = 0,A.newval = 0x10000
- 线程B:更新锁计数:检查该内存区域的独占访问标记已被清除,重新读取锁 lock 到临时变量 B.lockval 并设置该内存区域独占标记,此时的状态:
lock->tickets.owner =0,lock->tickets.next = 1,B.lockval.tickets.owner = 0,B.lockval.tickets.next = 1
9. 线程B:计算锁序号:对 B.newval = B.lockval.slock + (1<<16) = 0x10000 + 0x10000,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 1,B.lockval.tickets.owner = 0,B.lockval.tickets.next = 1,B.newval = 0x20000
- 线程B:更新锁计数:检查该内存区域的独占访问标记存在,将 B.newval 覆盖到 lock->slock 并清除独占标记,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 2,B.lockval.tickets.owner = 0,B.lockval.tickets.next = 1,B.newval = 0x20000
- 线程B:锁持有检测:由于 B.lockval.tickets.next != B.lockval.tickets.owner,因此进入 while 循环,不停的读取 lock->tickets.owner 并覆盖到 B.lockval.tickets.owner
lock->tickets.owner = 0,lock->tickets.next = 2,B.lockval.tickets.owner = 0,B.lockval.tickets.next = 1,B.newval = 0x20000
12. 线程C:计算锁序号:对 C.newval = C.lockval.slock + (1<<16) = 0x00 + 0x10000,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 0,C.lockval.tickets.owner = 0,C.lockval.tickets.next = 0,C.newval = 0x10000
- 线程C:更新锁计数:检查该内存区域的独占访问标记已被清除,重新读取锁 lock 到临时变量 C.lockval 并设置该内存区域独占标记,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 2,C.lockval.tickets.owner = 0,C.lockval.tickets.next = 2
14. 线程C:计算锁序号:对 C.newval = C.lockval.slock + (1<<16) = 0x20000 + 0x10000,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 2,C.lockval.tickets.owner = 0,C.lockval.tickets.next = 2,C.newval = 0x30000
- 线程C:更新锁计数:检查该内存区域的独占访问标记存在,将 C.newval 覆盖到 lock->slock 并清除独占标记,此时的状态:
lock->tickets.owner = 0,lock->tickets.next = 3,C.lockval.tickets.owner = 0,C.lockval.tickets.next = 2,C.newval = 0x30000
- 线程C:锁持有检测:由于 C.lockval.tickets.next != C.lockval.tickets.owner,因此进入 while 循环,不停的读取 lock->tickets.owner 并覆盖到 C.lockval.tickets.owner
lock->tickets.owner = 0,lock->tickets.next = 3,C.lockval.tickets.owner = 0,C.lockval.tickets.next = 2
至此,线程B 一直在等待 lock->tickets.owner 等于 1,而 线程C 则一直在等待 lock->tickets.owner 等于 2
- 线程A:访问完临界区后,调用 spin_unlock() 释放锁,lock->tickets.owner = lock->tickets.owner + 1
lock->tickets.owner = 1,lock->tickets.next = 3
- 线程B:锁持有检测:读取 lock->tickets.owner 并覆盖到 B.lockval.tickets.owner,由于 B.lockval.tickets.next == B.lockval.tickets.owner 相等则跳出 while 循环,正式持有锁返回了。
lock->tickets.owner = 1,lock->tickets.next = 3,B.lockval.tickets.owner = 1,B.lockval.tickets.next = 1
- 线程C:锁持有检测:继续读取 lock->tickets.owner 并覆盖到 C.lockval.tickets.owner,由于 C.lockval.tickets.next != C.lockval.tickets.owner,继续循环自旋。
lock->tickets.owner = 1,lock->tickets.next = 3,C.lockval.tickets.owner = 1,C.lockval.tickets.next = 2
- 线程B:访问完临界区后,调用 spin_unlock() 释放锁,lock->tickets.owner = lock->tickets.owner + 1
lock->tickets.owner = 2,lock->tickets.next = 3
- 线程C:锁持有检测:读取 lock->tickets.owner 并覆盖到 C.lockval.tickets.owner,由于 C.lockval.tickets.next == C.lockval.tickets.owner 相等则跳出 while 循环,正式持有锁返回了。
lock->tickets.owner = 2,lock->tickets.next = 3,C.lockval.tickets.owner = 2,C.lockval.tickets.next = 2
整个过程推演完毕,很巧妙的借助两个计数器和局部变量,实现等锁线程的有序排队,该思路也适用于应用程序开发。
自旋锁的原理总结
-
自旋锁是通过 ldrex、strex 来确保读写锁的计数器是原子操作的,这是 arm 芯片级实现的。
-
在上锁的过程中,会有两处循环,第一处是汇编指令循环, 第二处是 C 语言的 while 循环,两个循环的意义不一样:
-
汇编指令循环:实现对锁计数器的原子读写,确保得到的锁数据是最新的,锁的计数更新是准确无误的。
-
C语言的循环:实现锁在等待时的自旋功能,通过比较计数器,实现谁先等待锁,谁就先得到锁的有序排队。
以上两个循环的协同工作,前者实现原子操作和记录,后者实现了有序排队,完成了自旋锁的核心互斥功能。
-
【分析笔记】NXP PCF85263 设备驱动分析笔记
驱动移植
供应商无法提供相应的驱动程序,不过在 linux 最新的内核倒是有一份 pcf85363 的驱动,看代码并核对寄存器功能,是可以兼容 pcf85263 芯片。只是我们用的内核比较老 linux 4.9,rtc 子系统的接口有些变化,不能直接拿来用。根据 Linux 4.9 现有的驱动程序,修改了 pcf85363 驱动,可以正常的使用。
驱动框架
RTC 设备驱动
RTC 子系统的设备驱动还是非常简单的,只要按要求实现五个接口就能使用。
struct rtc_class_ops { ...... int (*read_time)(struct device *dev, struct rtc_time *tm); int (*set_time)(struct device *dev, struct rtc_time *tm); int (*read_alarm)(struct device *dev, struct rtc_wkalrm *alrm); int (*set_alarm)(struct device *dev, struct rtc_wkalrm *alrm); int (*alarm_irq_enable)(struct device *dev, unsigned int enabled); ...... };
通过以下接口即可注册到系统内:
devm_rtc_device_register(&client->dev, client->name, &rtc_ops, THIS_MODULE);rtc_class_ops->read_time(struct device *dev, struct rtc_time *tm) 1. 应用层通过 ioctl(fd, RTC_RD_TIME, &rtc_tm) 读取时间的回调接口。 2. 驱动层需要读取芯片里面的时间日期寄存器组,转换为十进制更新到 rtc_tm。 rtc_class_ops->set_time(struct device *dev, struct rtc_time *tm) 1. 应用层通过 ioctl(fd, RTC_SET_TIME, &rtc_tm) 设置时间的回调接口。 2. 驱动层需要停止芯片计时,并将 tm 转换为 BCD 更新到时间日期寄存器组。 rtc_class_ops->read_alarm(struct device *dev, struct rtc_wkalrm *alrm) 1. 应用层通过 ioctl(fd, RTC_ALM_READ, &rtc_tm) 读取闹钟的回调接口。 2. 驱动层需要读取芯片里面的 alarm 寄存器组,转换为十进制更新到 alrm->time。 3. 驱动层需要读取芯片里面的 alarm 控制寄存器,更新到 alrm->enabled。 rtc_class_ops->set_alarm(struct device *dev, struct rtc_wkalrm *alrm) 1. 应用层通过 ioctl(fd, RTC_ALM_SET, &rtc_tm) 设置闹钟的回调接口。 rtc_class_ops->alarm_irq_enable(struct device *dev, unsigned int enabled) 1. 应用层通过 ioctl(fd, RTC_AIE_ON/RTC_AIE_OFF, 0) 命令开启关闭闹钟中断的回调接口。 2. 应用层通过 read(fd, &data, sizeof(unsigned long))/select() 等待被中断唤醒。 3. 驱动层需要通过 GPIO 注册中断,并在中断里读取中断标志位,通过标志位来确定调用如下接口,唤醒应用程序。 rtc_update_irq(pcf85x63->rtc, 1, RTC_IRQF | RTC_AF);
应用层访问设备驱动的流程:
APP ---> rtc/rtc-dev.c(/dev/rtcX) ---> rtc/interface.c ---> pcf85263.c
RTC 设备驱动验证
方法一:使用现成的命令 hwclock
root@localhost:~# hwclock -f /dev/rtc0 --show 2022-10-20 09:30:12.679335+0800
方法二:自己编写应用程序验证
参考:linux-4.9\tools\testing\selftests\timers\rtctest.c
RTC 调试过程
一、测量硬件电压和时钟晶体
- 根据芯片手册,测量供电是否正常。
- 测量晶振频率是否为 32768 Hz。
- 测量 I2C 总线的上拉是否正常。
二、测试芯片能不能正常工作
查看寄存器手册,只要启动 RTC 时钟,再读取秒数寄存器,有累加即可确认正常。
root@localhost:~# i2cset -f -y 1 0x51 0x2e 0x00 root@localhost:~# i2cget -f -y 1 0x51 0x01 0x15 root@localhost:~# i2cget -f -y 1 0x51 0x01 0x16 root@localhost:~# i2cget -f -y 1 0x51 0x01 0x17
三、驱动移植
- 如果供应商有现成的驱动程序,当然是最快的。
- 如果供应商没有,则看看最新的内核有没有。
- 如果都没有,就拿相似的驱动程序根据芯片手册编写。
四、时间同步
- 设置系统时间从RTC启动和恢复
- 通过NTP同步方式设置RTC时间
make ARCH=arm64 menuconfig
Device Drivers --> [*] Real Time Clock --> [*] Set system time from RTC on startup and resume (rtc0) RTC used to set the system time [*] Set the RTC time based on NTP synchronization (rtc0) RTC used to synchronize NTP adjustment
驱动分析
一、初始化流程
- 从 DTS 获取配置的中断引脚,85263 通过引脚产生中断通知 SOC。
- 配置 0x2B 寄存器为 0x00, 清除所有的中断标志
- 配置 0x27 寄存器, 设置 INTA(7pin) 引脚作为中断输出引脚
- 申请中断并设置为低电平触发, 且在中断响应期间不重复触发(IRQF_ONESHOT)
- 按要求实现 rtc 的五个基本回调接口,并调用 devm_rtc_device_register() 注册到 RTC 子系统。
// 获取中断引脚 pcf85x63->irq_number = 0; gpio_config.gpio = of_get_named_gpio_flags(np, "int_port", 0, (enum of_gpio_flags *)(&gpio_config)); if (gpio_is_valid(gpio_config.gpio)){ pcf85x63->irq_gpio = gpio_config.gpio; pcf85x63->irq_number = gpio_to_irq(pcf85x63->irq_gpio); } if(0 == pcf85x63->irq_number){ dev_err(&client->dev, "get int gpio failed....\n"); return -EINVAL; } // 配置 0x2B 寄存器为 0x00, 清除所有的中断标志 regmap_write(pcf85x63->regmap, CTRL_FLAGS, 0); // 配置 0x27 寄存器, 设置 INTA(7pin) 引脚作为中断输出引脚 regmap_update_bits(pcf85x63->regmap, CTRL_PIN_IO, PIN_IO_INTA_OUT, PIN_IO_INTAPM); // 申请中断并设置为低电平触发(IRQF_TRIGGER_LOW), 且在中断响应期间不重复触发(IRQF_ONESHOT) ret = devm_request_threaded_irq(&client->dev, pcf85x63->irq_number, NULL, pcf85x63_rtc_handle_irq, IRQF_TRIGGER_LOW | IRQF_ONESHOT, client->name, client); if (ret) { dev_warn(&client->dev, "unable to request irq, alarms disabled\n"); return -EINVAL; } // 注册 RTC 设备 pcf85x63->client = client; i2c_set_clientdata(client, pcf85x63); pcf85x63->rtc = devm_rtc_device_register(&client->dev, client->name, &rtc_ops, THIS_MODULE); if (IS_ERR(pcf85x63->rtc)){ dev_err(&client->dev, "register rtc device failed....\n"); return PTR_ERR(pcf85x63->rtc); }
二、读取时间的实现
- 一次性读出时间日期寄存器组(00h ~ 07h)。
- 由于芯片寄存器是以 BCD 方式存储,所以需要转换为十进制,并复制到 tm。
static int pcf85x63_rtc_read_time(struct device *dev, struct rtc_time *tm) { struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev); unsigned char buf[DT_YEARS + 1]; int ret, len = sizeof(buf); // 一次性读出时间日期寄存器组(00h ~ 07h) if ((ret = regmap_bulk_read(pcf85x63->regmap, DT_100THS, buf, len))) { dev_err(dev, "%s: error %d\n", __func__, ret); return ret; } // 通过 BCD 转换 tm->tm_year = bcd2bin(buf[DT_YEARS]); tm->tm_year += 100; // adjust for 1900 base of rtc_time tm->tm_wday = buf[DT_WEEKDAYS] & 7; buf[DT_SECS] &= 0x7F; tm->tm_sec = bcd2bin(buf[DT_SECS]); buf[DT_MINUTES] &= 0x7F; tm->tm_min = bcd2bin(buf[DT_MINUTES]); tm->tm_hour = bcd2bin(buf[DT_HOURS]); tm->tm_mday = bcd2bin(buf[DT_DAYS]); tm->tm_mon = bcd2bin(buf[DT_MONTHS]) - 1; return 0; }
三、设置时间的实现
- 通过设置 0x2E 寄存器来切断外部时钟的分配器,实现停止计时。
- 通过设置 0x2F 寄存器重置预分频器。
- 将应用层传下的时间转换为 BCD 更新到时间日期寄存器组(00h ~ 07h)
- 配置 0x2E 寄存器为 0x00, 开始计时。
static int pcf85x63_rtc_set_time(struct device *dev, struct rtc_time *tm) { struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev); unsigned char tmp[11] = {0}; unsigned char *buf = &tmp[2]; int ret; // 要设置时间之前需要做的事情 tmp[0] = STOP_EN_STOP; // 配置 0x2E 寄存器为 0x01, 切断时钟, 停止计数( RTC clock is stopped) tmp[1] = RESET_CPR; // 配置 0x2F 寄存器为 0xA4,重置预分频器 if((ret = regmap_bulk_write(pcf85x63->regmap, CTRL_STOP_EN, tmp, 2))) return ret; // 将时间转换为 BCD 更新到时间日期寄存器组(00h ~ 07h) buf[DT_100THS] = 0; buf[DT_SECS] = bin2bcd(tm->tm_sec); buf[DT_MINUTES] = bin2bcd(tm->tm_min); buf[DT_HOURS] = bin2bcd(tm->tm_hour); buf[DT_DAYS] = bin2bcd(tm->tm_mday); buf[DT_WEEKDAYS] = tm->tm_wday; buf[DT_MONTHS] = bin2bcd(tm->tm_mon + 1); buf[DT_YEARS] = bin2bcd(tm->tm_year % 100); if(regmap_bulk_write(pcf85x63->regmap, DT_100THS, buf, sizeof(tmp) - 2)) return ret; // 配置 0x2E 寄存器为 0x00, 开始计时( RTC clock runs) return regmap_write(pcf85x63->regmap, CTRL_STOP_EN, 0); }
四、读取闹钟的回调
- 一次性读出闹钟寄存器组(08h ~ 0Ch)。
- 将寄存器的 BCD 数据转换为十进制复制到 alrm->time。
- 读取 0x29 闹钟寄存器, 查询闹钟中断是否被使能。
- 如果闹钟中断被使能则通过更新 alrm->enabled 成员让应用层知道。
static int pcf85x63_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm) { struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev); unsigned char buf[DT_MONTH_ALM1 - DT_SECOND_ALM1 + 1]; unsigned int val; int ret; // 一次性读出闹钟寄存器组(08h ~ 0Ch) if ((ret = regmap_bulk_read(pcf85x63->regmap, DT_SECOND_ALM1, buf, sizeof(buf)))) return ret; alrm->time.tm_sec = bcd2bin(buf[0]); alrm->time.tm_min = bcd2bin(buf[1]); alrm->time.tm_hour = bcd2bin(buf[2]); alrm->time.tm_mday = bcd2bin(buf[3]); alrm->time.tm_mon = bcd2bin(buf[4]) - 1; // 读取 0x29 闹钟寄存器, 查询闹钟中断是否被使能 if ((ret = regmap_read(pcf85x63->regmap, CTRL_INTA_EN, &val))) return ret; // 如果闹钟中断被使能则通过更新 alrm->enabled 成员让应用层知道 alrm->enabled = !!(val & INT_A1IE); return 0; }
五、设置闹钟的回调
- 关闭闹钟中断,避免设置过程中触发中断。
- 将应用层传下来的时间转化为 BCD,并更新到闹钟寄存器组(08h ~ 0Ch)。
- 根据应用层传下来的 alrm->enabled 决定是否再次打开闹钟中断。
static int pcf85x63_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm) { struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev); unsigned char buf[DT_MONTH_ALM1 - DT_SECOND_ALM1 + 1]; int ret; // 转换为 BCD buf[0] = bin2bcd(alrm->time.tm_sec); buf[1] = bin2bcd(alrm->time.tm_min); buf[2] = bin2bcd(alrm->time.tm_hour); buf[3] = bin2bcd(alrm->time.tm_mday); buf[4] = bin2bcd(alrm->time.tm_mon + 1); // 在设置时间之前先把中断关闭, 避免误触发中断 if ((ret = _pcf85x63_rtc_alarm_irq_enable(pcf85x63, 0))) return ret; // 将时更新到闹钟寄存器组(08h ~ 0Ch) if ((ret = regmap_bulk_write(pcf85x63->regmap, DT_SECOND_ALM1, buf, sizeof(buf)))) return ret; // 根据应用层的设置, 决定是否启用闹钟中断 return _pcf85x63_rtc_alarm_irq_enable(pcf85x63, alrm->enabled); }
六、启用关闭闹钟中断
- 配置 0x10 寄存器, 启用/关闭 时、分、秒、日、月、的闹钟功能
- 配置 0x29 闹钟寄存器, 启用/关闭 闹钟中断, 上述月、日、时、分、秒有闹钟事件会触发中断
- 清除闹钟中断标志
static int _pcf85x63_rtc_alarm_irq_enable(struct pcf85x63 *pcf85x63, unsigned int enabled) { int ret; unsigned int alarm_flags = ALRM_SEC_A1E | ALRM_MIN_A1E | ALRM_HR_A1E | ALRM_DAY_A1E | ALRM_MON_A1E; // 配置 0x10 寄存器, 启用/关闭 时、分、秒、日、月、的闹钟功能 ret = regmap_update_bits(pcf85x63->regmap, DT_ALARM_EN, alarm_flags, enabled ? alarm_flags : 0); if (ret){ return ret; } // 配置 0x29 闹钟寄存器, 启用/关闭 闹钟中断, 上述月、日、时、分、秒有闹钟事件会触发中断 ret = regmap_update_bits(pcf85x63->regmap, CTRL_INTA_EN, INT_A1IE, enabled ? INT_A1IE : 0); if (ret || enabled){ return ret; } // 清除闹钟中断标志 return regmap_update_bits(pcf85x63->regmap, CTRL_FLAGS, FLAGS_A1F, 0); } static int pcf85x63_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled) { struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev); return _pcf85x63_rtc_alarm_irq_enable(pcf85x63, enabled); }
七、闹钟中断服务程序
- 读取 0x2B 寄存器, 得到所有的中断标志。
- 如果是闹钟的中断(FLAGS_A1F),则调用 rtc_update_irq() 唤醒应用层,并清除闹钟中断标志位。
static irqreturn_t pcf85x63_rtc_handle_irq(int irq, void *dev_id) { struct pcf85x63 *pcf85x63 = i2c_get_clientdata(dev_id); unsigned int flags; // 读取 0x2B 寄存器, 得到所有的中断标志 if (regmap_read(pcf85x63->regmap, CTRL_FLAGS, &flags)) return IRQ_NONE; // 如果是闹钟的中断 if (flags & FLAGS_A1F) { // 通知应用层有闹钟 rtc_update_irq(pcf85x63->rtc, 1, RTC_IRQF | RTC_AF); // 清除闹钟中断标志 regmap_update_bits(pcf85x63->regmap, CTRL_FLAGS, FLAGS_A1F, 0); return IRQ_HANDLED; } return IRQ_NONE; }
-
回复: D1H支持1920*1200分辨率,但是1080*1920竖屏支持吗
@xiaoxiao 我遇过类似的问题,开机 logo 的分辨力如果比显示屏大,就会出现问题,当时在 UBOOT 似乎未实现图片缩放的功能。(现在好像也没有)
-
回复: XR806下载freertos镜像verify boot error
@yanhui5329 这只有全志的工程师能回答了。我也是经常遇到这种问题,搞出经验来了。论坛有这种烧录不了的问答记录,但没提到这种情况。
-
回复: 新手求教T113麻雀 怎么调试rgb屏幕,用的是官方的tina-sdk能具体讲一下步骤最好
@yuzukitsuru 迄今为止,我见过最详细最完整的说明。666...
-
回复: 【分析笔记】Linux I2C-Tools 使用踩坑笔记
@dream 在 【分析笔记】Linux I2C-Tools 使用踩坑笔记 中说:
@daizebin sht41 的器件地址是 0x44,就决定了在自动模式下,是以写方式检测,所以可以识别到,但是 sgp41 的器件地址是 0x59,落在了以读方式检测的范围,所以无法识别到。具体看本文 scan_i2c_bus() 的源码截图就能看出来。
再补充一下,不是所有的 I2C 器件都必须要以写的方式才能检测出来,只有少部分,如 sht41\sgp41 这类器件比较特别,需要先写再读才有应答,正因为大部分 I2C 器件无论是读写都会有应答,所以才会因惯性思维,导致踩坑。
-
回复: 【D1s & WIFI】简单测试下哪吒MQ的WiFi(后边补上移植记录)
WIFI 天线座子距离 USB 那么近,况且天线还通过过孔到板的另外一面,咋看起来没啥影响。厉害厉害。
-
回复: 编译应用程序报错arm-openwrt-linux-muslgnueabi/bin/ld: cannot find -lanl
@dql2016 make menuconfig 里面可以选
-
回复: 【分析笔记】Linux I2C-Tools 使用踩坑笔记
@daizebin sht41 的器件地址是 0x44,就决定了在自动模式下,是以写方式检测,所以可以识别到,但是 sgp41 的器件地址是 0x59,落在了以读方式检测的范围,所以无法识别到。具体看本文 scan_i2c_bus() 的源码截图就能看出来。
-
回复: F133A(D1s)如何使用GPIO:PG4 PG5?
@casojie 我的不是 F133A 的平台,我没有 D1 平台,所以不知道具体路径, 你可以 find 下看看,如果没有的话,试试:mount -t debugfs debug /proc/sys/debug
-
回复: F133A(D1s)如何使用GPIO:PG4 PG5?
@casojie 直接用 sunxi_pinctrl 吧,不用去计算。
下面是简单的使用方法,平时调试特别方便,实现原理是直接操作寄存器,所以不受驱动影响。
查看引脚当前配置:
root@sun8i:/proc/sys/debug/sunxi_pinctrl# echo PE24 > sunxi_pin
echo PE24 > sunxi_pinroot@sun8i:/proc/sys/debug/sunxi_pinctrl# cat sunxi_pin_configure
cat sunxi_pin_configure
printf register value...
pin[PE24] function: 1; register addr: 0xf1c2089c
pin[PE24] data: 1(default value : 0); register addr: 0xf1c208a0
pin[PE24] dleve: 1(default value : 1); register addr: 0xf1c208a8
pin[PE24] pull: 0(default value : 0); register addr: 0xf1c208b0设置引脚为低电平输出:(注意要先设置 function )
root@sun8i:/proc/sys/debug/sunxi_pinctrl# echo PE24 0 > data
echo PE24 0 > dataroot@sun8i:/proc/sys/debug/sunxi_pinctrl# cat sunxi_pin_configure
cat sunxi_pin_configure
printf register value...
pin[PE24] function: 1; register addr: 0xf1c2089c
pin[PE24] data: 0(default value : 0); register addr: 0xf1c208a0
pin[PE24] dleve: 1(default value : 1); register addr: 0xf1c208a8
pin[PE24] pull: 0(default value : 0); register addr: 0xf1c208b0设置引脚为高电平输出:
root@sun8i:/proc/sys/debug/sunxi_pinctrl# echo PE24 1 > data
echo PE24 1 > dataroot@sun8i:/proc/sys/debug/sunxi_pinctrl# cat sunxi_pin_configure
cat sunxi_pin_configure
printf register value...
pin[PE24] function: 1; register addr: 0xf1c2089c
pin[PE24] data: 1(default value : 0); register addr: 0xf1c208a0
pin[PE24] dleve: 1(default value : 1); register addr: 0xf1c208a8
pin[PE24] pull: 0(default value : 0); register addr: 0xf1c208b0 -
【分析笔记】Linux I2C-Tools 使用踩坑笔记
一、踩坑缘由
在调试 I2C 器件时,我一般习惯于使用 i2cdetect 工具来确认芯片是否有应答,通常有应答之后,就会开始着手移植或者编写对应的驱动程序,但是在调试 sgp41 传感器时却不灵了。
二、问题现象
在连续完成多个 I2C 器件的调试和驱动开发之后,最后一个 sgp41 传感器却一直无法被检测到。在使用示波器再次测量芯片供电、检查I2C波形、引脚顺序、电平匹配都正确后,认为是芯片坏了,换了多颗芯片,都无法识别,寄给供应商,供应商又说检测良好,这就很神奇了。
在同一个座子上,sht41 都能正常被检测到,但是 sgp41 却无法检测,更何况该总线上还挂了其它的 I2C 器件都能准确检测出来。
三、问题分析
百思不得其解,在仔细观察逻辑分析仪解析的结果,发现在检测 0x44(sht41) 器件地址的时候,i2cdetect 使用的是采用写的方式检测,而检测 0x59(sgp41) 器件地址时,采用读的方式检测。
由于之前调试过 sht41 器件,知道这类传感器需要先写再读,才会有应答信号(规格书没有体现出来),因此推测跟这个有关。手写了一个 I2C 设备驱动,先写再读取,发现可以正常通信,证实了我的猜测。
四、深入分析
我特别好奇的是,为什么 i2cdetect 工具会对不同的地址段采用不同的方式进行检测,我分析了 i2cdetect.c 源代码,发现默认是以自动模式检测,而自动模式则会根据不同的地址段采取读或写来实现检测。
源码下载:http://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/i2c-tools-4.0.tar.gz
如果需要检测类似 sht41、sgp41 这种必须要先写再读才会有应答的芯片,就必须要指定检测模式为写检测(MODE_QUICK),由于 sht41 的器件地址 0x44 恰好在自动模式中以写的方式检测,因此可以检测得到。通过分析源码发现,-r 和 -q 即可指定读写模式:
下图为指定以写的方式检测 sgp41(0x59),可以发现有应答信号,被检测出来了:
五、经验总结
- 不是所有的 I2C 器件都会直接响应读请求。
- 使用 i2cdetect 工具检测芯片,建议使用 -q 和 -r 参数都试试。
-
回复: 试一试用 全志 AIC800 LCD0 PD0-PD9 驱动 15寸的LVDS电脑显示屏
@duanzhh 在 试一试用 全志 AIC800 LCD0 PD0-PD9 驱动 15寸的LVDS电脑显示屏 中说:
AIC800上ubuntu18.04的xfce桌面
咋弄的,是有什么公开的资料,可以实现 AIC800 跑 Ubuntu 18.04 ,使用 xfce 作为桌面环境嘛?
-
回复: XR806烧录失败
@flyeagle
这个错误提示,指的是握手失败,即串口通讯失败,建议按照如下几步排查:- 检查电脑的串口号是否选对。
- 检查线序,TX\RX\GND 是否正确。
- 检查供电,XR806 的 VBAT 供电是否正常。
- 检查复位,XR806 的复位引脚,低复位,烧录时需为高电平。
-
【分析笔记】全志平台 TWI 上拉电压异常的问题
记录说明
原本这么简单的芯片,没有必要做记录,后来发现其中有一颗单独挂在 TWI2 无法通信,而主要原因是最容易忽视的电源域的问题,因此记录一下这件事情。
芯片介绍
MCP3021 是一颗 10BIT 的 ADC 器件,直接通过 I2C 读取两个字节的数据即可获取到 10BIT 的数值。
分析过程
内核版本:Linux version 4.9.56
幸得全志在线的 Syter 提醒了一下,不然估计还得捣鼓一阵,在此感谢 Syter:
- 使用 i2cdetect -y 2 无法检测到芯片,意味着无法通信。
- TWI2 与其它 TWI 的一样都是通过 10K 上拉到 3.3V,但是测量发现只有 TWI2 是 1.5V。
- 通过手动设置 PE14\PE15 为 GPIO 口模式,并设置输入,测量只有 1.5V。
- 通过手动设置 PE14\PE15 为 GPIO 口模式,并设置输出高电平,测量只有 1.2V。
- 拆除唯一一颗挂在 TWI2 上面的 MCP3021A5T-E 芯片,测试结果和上面一样。
- 后发现 TWI2 与 TWI0\TWI1 唯一不同的是不 TWI2 属于 PE 组,其它共属 PH 组。
- 测量 PE 组 GPIO 的电源域,只有 1V 左右,通过设置并使能 3.3V VCC-PE,测量上拉电压正常。
- 经过测试,可以正常通信,问题得到解决,PE 组没有供电,测量的 1V 应该是通过上拉电阻串进去的电。
如何使能指定组的 GPIO 电源域,取决于实际电路从哪取的电源,如果是 PMU 的某一路电源,只需启用该路电压并设置为 1003300 即可,如:
[power_sply] ...... gpio1_vol = 1003300
应用方法
一、确认器件地址
型号 器件地址 MCP3021A0T-E 0x48 MCP3021A1T-E 0x49 MCP3021A2T-E 0x4A MCP3021A3T-E 0x4B MCP3021A4T-E 0x4C MCP3021A5T-E 0x4D MCP3021A6T-E 0x4E MCP3021A7T-E 0x4F 二、启用 I2C 控制器
sys_config.fex
[twi2] twi2_used = 1 twi2_scl = port:PE14<3><default><default><default> twi2_sda = port:PE15<3><default><default><default> [twi2_suspend] twi2_scl = port:PE14<7><default><default><default> twi2_sda = port:PE15<7><default><default><default>
三、命令行测试验证
数据读取:
root@localhost:~# i2ctransfer -y -f 0 r2@0x4d 0x01 0x2c
数据解析:
1. 高字节:0x01 --> 0000 0001 --> 取低四位:0001 2. 低字节:0x2c --> 0010 1100 --> 取高六位:0010 11 3. 合并字节:0001 0010 11 --> 0001001011 --> 75 4. 换算电压:75 * 3.3 / (2^10) = 0.24V 与万用表测量电压基本一致。
-
【分析笔记】全志平台 gpio_wdt 驱动应用和 stack crash 解决
使用说明
第一次遇到看门狗芯片是通过切换电平信号来喂狗,如 SGM706 芯片,之前也比较少会用到看门狗芯片。原本打算参考 sunxi-wdt.c 的框架,利用定时器自己写一个,无意中发现内核已经有 gpio_wdt.c 驱动程序,其原理也是通过内核定时器实现喂狗。因其使用了 of_get_gpio_flags() 接口获取 GPIO 信息,和 gpio-keys.c 驱动一样,该接口存在内存越界的问题,需要略作修改才能使用。
内核配置
内核版本:Linux 4.9
make ARCH=arm64 menuconfig
Device Drivers ---> [*] Watchdog Timer Support ---> <*> LED Support for GPIO connected LEDs <*> Watchdog device controlled through GPIO-line
配置文件
sys_config.fex
全志平台便捷方式配置,不过新平台不再支持 sys_config.fex了,也可以使用通用的 dts 配置方式
;---------------------------------------------------------------------------------- ;gpio-wdt parameters ;compatible: 匹配平台驱动 ;hw_algo: 清除看门狗计数方式:切换方式(toggle)或脉冲方式(level) ;hw_margin_ms: 看门狗电路会触发复位的最长时间(毫秒),不能小于 2 或者大于 65535 ;always-running: 如果看门狗不能关闭,使能后驱动默认会自动喂狗,在应用层调用 STOP 接口不会执行关闭 ;gpios: 连接看门狗芯片 WDI 的引脚 ;---------------------------------------------------------------------------------- [wdt-gpio] compatible = "linux,wdt-gpio" hw_algo = "level" hw_margin_ms = 1600 always-running = "true" gpios = port:PL12<1><default><default><default>
配置选项说明:linux-4.9\Documentation\devicetree\bindings\watchdog\gpio_wdt.txt
* GPIO-controlled Watchdog Required Properties: - compatible: Should contain "linux,wdt-gpio". - gpios: From common gpio binding; gpio connection to WDT reset pin. - hw_algo: The algorithm used by the driver. Should be one of the following values: - toggle: Either a high-to-low or a low-to-high transition clears the WDT counter. The watchdog timer is disabled when GPIO is left floating or connected to a three-state buffer. - level: Low or high level starts counting WDT timeout, the opposite level disables the WDT. Active level is determined by the GPIO flags. - hw_margin_ms: Maximum time to reset watchdog circuit (milliseconds). Optional Properties: - always-running: If the watchdog timer cannot be disabled, add this flag to have the driver keep toggling the signal without a client. It will only cease to toggle the signal when the device is open and the timeout elapsed. Example: watchdog: watchdog { /* ADM706 */ compatible = "linux,wdt-gpio"; gpios = <&gpio3 9 GPIO_ACTIVE_LOW>; hw_algo = "toggle"; hw_margin_ms = <1600>; };
内存越界
日志信息
[ 3.727333] sunxi-wdt 1c20ca0.watchdog: Watchdog enabled (timeout=16 sec, nowayout=0) [ 3.736874] of_get_named_gpiod_flags: parsed 'gpios' property of node '/soc@01c00000/wdt-gpio[0]' - status (0) [ 3.748392] Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: ffffff8008655044 [ 3.748392] [ 3.760564] CPU: 2 PID: 1 Comm: swapper/0 Not tainted 4.9.56 #179 [ 3.767382] Hardware name: sun50iw1 (DT) [ 3.771771] Call trace: [ 3.774522] [<ffffff800808a7d4>] dump_backtrace+0x0/0x22c [ 3.780566] [<ffffff800808aa24>] show_stack+0x24/0x30 [ 3.786226] [<ffffff80083c9a64>] dump_stack+0x8c/0xb0 [ 3.791880] [<ffffff80081a5960>] panic+0x14c/0x298 [ 3.797247] [<ffffff80080a19d0>] print_tainted+0x0/0xa8 [ 3.803099] [<ffffff8008655044>] gpio_wdt_probe+0x230/0x258 [ 3.809338] [<ffffff80084b3e9c>] platform_drv_probe+0x60/0xac [ 3.815770] [<ffffff80084b1a6c>] driver_probe_device+0x1b8/0x3d4 [ 3.822493] [<ffffff80084b1d1c>] __driver_attach+0x94/0x108 [ 3.828729] [<ffffff80084af68c>] bus_for_each_dev+0x88/0xc8 [ 3.834964] [<ffffff80084b1374>] driver_attach+0x30/0x3c [ 3.840908] [<ffffff80084b0d24>] bus_add_driver+0xf8/0x24c [ 3.847046] [<ffffff80084b2a74>] driver_register+0x9c/0xe8 [ 3.853186] [<ffffff80084b3de0>] __platform_driver_register+0x5c/0x68 [ 3.860400] [<ffffff8008d48a20>] gpio_wdt_driver_init+0x18/0x20 [ 3.867026] [<ffffff8008083a2c>] do_one_initcall+0xb0/0x14c [ 3.873267] [<ffffff8008d10d64>] kernel_init_freeable+0x14c/0x1e8 [ 3.880093] [<ffffff8008970724>] kernel_init+0x18/0x104 [ 3.885941] [<ffffff8008083050>] ret_from_fork+0x10/0x40 [ 3.891884] SMP: stopping secondary CPUs [ 3.896276] Kernel Offset: disabled [ 3.900178] Memory Limit: none [ 3.909173] Rebooting in 5 seconds..
解决补丁
--- linux-4.9\drivers\watchdog\gpio_wdt.c 2022-09-13 17:51:57.000000000 +0800 +++ linux-4.9\drivers\watchdog\gpio_wdt.c 2022-09-13 17:51:37.000000000 +0800 @@ -12,12 +12,13 @@ #include <linux/err.h> #include <linux/delay.h> #include <linux/module.h> #include <linux/of_gpio.h> #include <linux/platform_device.h> #include <linux/watchdog.h> +#include <linux/sunxi-gpio.h> #define SOFT_TIMEOUT_MIN 1 #define SOFT_TIMEOUT_DEF 60 #define SOFT_TIMEOUT_MAX 0xffff enum { @@ -138,29 +139,29 @@ .set_timeout = gpio_wdt_set_timeout, }; static int gpio_wdt_probe(struct platform_device *pdev) { struct gpio_wdt_priv *priv; - enum of_gpio_flags flags; + struct gpio_config gpio_flags; unsigned int hw_margin; unsigned long f = 0; const char *algo; int ret; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; platform_set_drvdata(pdev, priv); - priv->gpio = of_get_gpio_flags(pdev->dev.of_node, 0, &flags); + priv->gpio = of_get_gpio_flags(pdev->dev.of_node, 0, (enum of_gpio_flags *)&gpio_flags); if (!gpio_is_valid(priv->gpio)) return priv->gpio; - priv->active_low = flags & OF_GPIO_ACTIVE_LOW; + priv->active_low = gpio_flags.data & OF_GPIO_ACTIVE_LOW; ret = of_property_read_string(pdev->dev.of_node, "hw_algo", &algo); if (ret) return ret; if (!strcmp(algo, "toggle")) { priv->hw_algo = HW_ALGO_TOGGLE;
原因分析
- gpio_wdt 驱动使用 of_get_gpio_flags() 获取 dts 里面 gpio 配置信息。
- 但是 of_get_gpio_flags() 传入 enum of_gpio_flags 类型来获取配置信息。
- of_get_gpio_flags() 的最终实现由具体的 SOC 厂商实现,这里是全志厂商实现。
- 实现的函数为:drivers/pinctrl/sunxi/pinctrl-sunxi.c --> sunxi_pinctrl_gpio_of_xlate()。
- 在 sunxi_pinctrl_gpio_of_xlate() 却是通过强制转换 struct gpio_config 类型存储 gpio 配置信息。
- enum of_gpio_flags 占用 4 字节,而 struct gpio_config 占用 20 字节,出现内存越界操作的问题。
// include/linux/of_gpio.h enum of_gpio_flags { OF_GPIO_ACTIVE_LOW = 0x1, OF_GPIO_SINGLE_ENDED = 0x2, }; // 4Byte // include/linux/sunxi-gpio.h struct gpio_config { u32 gpio; u32 mul_sel; u32 pull; u32 drv_level; u32 data; }; // 20Byte // drivers/pinctrl/sunxi/pinctrl-sunxi.c static int sunxi_pinctrl_gpio_of_xlate(struct gpio_chip *gc, const struct of_phandle_args *gpiospec, u32 *flags) { struct gpio_config *config; int pin, base; base = PINS_PER_BANK * gpiospec->args[0]; pin = base + gpiospec->args[1]; pin = pin - gc->base; if (pin > gc->ngpio) return -EINVAL; if (flags) { // 问题出在这个条件下面的赋值语句 // 传进来的是 enum of_gpio_flags,只有 4Byte // 结果使用的 struct gpio_config,却有 20Byte config = (struct gpio_config *)flags; config->gpio = base + gpiospec->args[1]; config->mul_sel = gpiospec->args[2]; config->drv_level = gpiospec->args[3]; config->pull = gpiospec->args[4]; config->data = gpiospec->args[5]; } return pin; }
-
【分析笔记】Linux gpio_wdt.c 看门狗设备驱动源码分析
基本原理
该看门狗的设备驱动实现原理很简单,比较主要的有两点:
一、定时器喂狗
通过定时器根据配置文件配置的喂狗方式(如脉冲切换、电平切换),对指定的 gpio 进行脉冲切换或电平切换实现喂狗。
-
脉冲切换
指的是喂狗时,会给 gpio 一个 1us 宽度的高电平或低电平(取决于配置的 gpio 电平状态)。如设置为 1600ms,那么每 800ms 就会产生一个这样的脉冲信号。 -
电平切换
指的是喂狗时,会以固定的时间间隔,翻转 gpio 的电平状态,如设置看门狗超时时间为 1600ms,那么每 800ms 就会翻转一次 gpio 的电平状态,实现喂狗。
二、软硬件喂狗时间解耦
驱动将喂狗时间分为硬件喂狗时间和软件喂狗时间,很好的解决了软硬件时间的耦合问题,对上提供一个统一的喂狗时间,不受硬件芯片的实际喂狗时间限制,应用软件设计时不需要考虑底层采用了什么硬件。
在应用软件启动喂狗之后,驱动会启动定时器按照硬件看门狗芯片的最长超时复位时间的一半进行喂狗,如看门狗在 1600ms 内没有收到喂狗信号,就会产生复位动作,那么驱动就会取 1600ms 的一半,即定时器设置 800ms 为周期进行喂狗。
而应用软件可以设置喂狗时间范围为 1s ~ 65535s,驱动默认为 60s,如果应用软件没有在所设置的时间内调用 WDIOC_KEEPALIVE 进行喂狗(如60s) ,那么驱动程序就会停止给硬件喂狗,从而让硬件看门狗芯片产生复位信号,也就是说在 60s 内,驱动还是会通过定时器给硬件看门狗芯片继续喂狗,超过 60s 后,应用没有喂狗,那么定时器就会被停止。
-
硬件喂狗时间
不同的硬件看门狗芯片的看门狗超时时间都不一样,如 1.6s,那么就需要在 1.6s 内切换一次 gpio 信号进行喂狗,驱动取的是一半的时间,即 1.6s / 2 = 0.8s 喂狗,以便于留有一定的冗余时间。 -
软件喂狗时间
指的是应用程序进行喂狗的时间,内核预设最短时间为 1s,最大时间为 65535s,默认喂狗时间为 60s。
// 应用层进行喂狗的回调: 对应应用层 WDIOC_KEEPALIVE static int gpio_wdt_ping(struct watchdog_device *wdd) { // 更新最后一次喂狗时间 struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); priv->last_jiffies = jiffies; return 0; }
static void gpio_wdt_hwping(unsigned long data) { struct watchdog_device *wdd = (struct watchdog_device *)data; struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); // 如果应用层已经启动喂狗, 则判断是否超出喂狗时间 // 注意这里的喂狗时间仅仅只是软件驱动喂狗时间,而不是硬件看门狗时间。 // 这里的好处是可以将应用层的喂狗时间与硬件看门狗喂狗的时间解耦出来。 // 若软件喂狗时间超时,那不继续喂硬件看门狗就可以让硬件看门狗复位了。 if (priv->armed && time_after(jiffies, priv->last_jiffies + msecs_to_jiffies(wdd->timeout * 1000))) { dev_crit(wdd->parent, "Timer expired. System will reboot soon!\n"); return; } // 重置定时器, 使之可以继续定时 mod_timer(&priv->timer, jiffies + priv->hw_margin); // 根据喂狗方式, 选择电平切换方式或电平脉冲方式喂狗 switch (priv->hw_algo) { case HW_ALGO_TOGGLE: /* Toggle output pin */ priv->state = !priv->state; // 使用电平切换方式 gpio_set_value_cansleep(priv->gpio, priv->state); break; case HW_ALGO_LEVEL: /* Pulse */ // 使用电平脉冲方式 gpio_set_value_cansleep(priv->gpio, !priv->active_low); udelay(1); gpio_set_value_cansleep(priv->gpio, priv->active_low); break; } }
源码分析
以下源码对原生的 Linux 驱动在调用 of_get_gpio_flags() 时,为了适应全志平台做了一点小改动。
/* * Driver for watchdog device controlled through GPIO-line * * Author: 2013, Alexander Shiyan <shc_work@mail.ru> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. */ #include <linux/err.h> #include <linux/delay.h> #include <linux/module.h> #include <linux/of_gpio.h> #include <linux/platform_device.h> #include <linux/watchdog.h> #include <linux/sunxi-gpio.h> #define SOFT_TIMEOUT_MIN 1 #define SOFT_TIMEOUT_DEF 60 #define SOFT_TIMEOUT_MAX 0xffff enum { HW_ALGO_TOGGLE, // 切换方式 HW_ALGO_LEVEL, // 脉冲方式 }; struct gpio_wdt_priv { int gpio; bool active_low; bool state; bool always_running; bool armed; unsigned int hw_algo; unsigned int hw_margin; unsigned long last_jiffies; struct timer_list timer; struct watchdog_device wdd; }; static void gpio_wdt_disable(struct gpio_wdt_priv *priv) { // 根据电平有效性来决定设置什么电平,来关闭看门狗 gpio_set_value_cansleep(priv->gpio, !priv->active_low); // 如果是电平切换方式,则恢复输入状态 if (priv->hw_algo == HW_ALGO_TOGGLE) gpio_direction_input(priv->gpio); } static void gpio_wdt_hwping(unsigned long data) { struct watchdog_device *wdd = (struct watchdog_device *)data; struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); // 如果应用层已经启动喂狗, 则判断是否超出喂狗时间 // 注意这里的喂狗时间仅仅只是软件驱动喂狗时间,而不是硬件看门狗时间。 // 这里的好处是可以将应用层的喂狗时间与硬件看门狗喂狗的时间解耦出来。 // 若软件喂狗时间超时,那不继续喂硬件看门狗就可以让硬件看门狗复位了。 if (priv->armed && time_after(jiffies, priv->last_jiffies + msecs_to_jiffies(wdd->timeout * 1000))) { dev_crit(wdd->parent, "Timer expired. System will reboot soon!\n"); return; } // 重置定时器, 使之可以继续定时 mod_timer(&priv->timer, jiffies + priv->hw_margin); // 根据喂狗方式, 选择电平切换方式或电平脉冲方式喂狗 switch (priv->hw_algo) { case HW_ALGO_TOGGLE: /* Toggle output pin */ priv->state = !priv->state; // 使用电平切换方式 gpio_set_value_cansleep(priv->gpio, priv->state); break; case HW_ALGO_LEVEL: /* Pulse */ // 使用电平脉冲方式 gpio_set_value_cansleep(priv->gpio, !priv->active_low); udelay(1); gpio_set_value_cansleep(priv->gpio, priv->active_low); break; } } // 驱动层自行启动看门狗, 区别于应用层启动看门狗 static void gpio_wdt_start_impl(struct gpio_wdt_priv *priv) { priv->state = priv->active_low; gpio_direction_output(priv->gpio, priv->state); priv->last_jiffies = jiffies; // 这里调用了定时器的回调函数, 内部有 mod_timer() 可以实现启动定时器 gpio_wdt_hwping((unsigned long)&priv->wdd); } // 应用层启动看门狗的回调, 对应应用层 WDIOC_SETOPTIONS->WDIOS_ENABLECARD static int gpio_wdt_start(struct watchdog_device *wdd) { struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); gpio_wdt_start_impl(priv); priv->armed = true; // 区分应用层启动还是设备驱动启动的看门狗 return 0; } // 应用层关闭看门狗的回调, 对应应用层 WDIOC_SETOPTIONS->WDIOS_DISABLECARD static int gpio_wdt_stop(struct watchdog_device *wdd) { struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); priv->armed = false; // 如果配置 always_running = true 则不会停止定时器喂狗 // 如果没有配置, 或者配置 always_running = false, 就会停止定时器喂狗并关闭看门狗 if (!priv->always_running) { mod_timer(&priv->timer, 0); // 停止定时器 gpio_wdt_disable(priv); // 关闭看门狗 } return 0; } // 应用层进行喂狗的回调: 对应应用层 WDIOC_KEEPALIVE static int gpio_wdt_ping(struct watchdog_device *wdd) { // 更新最后一次喂狗时间 struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); priv->last_jiffies = jiffies; return 0; } // 应用层设置超时时间的回调: 对应应用层 WDIOC_SETTIMEOUT static int gpio_wdt_set_timeout(struct watchdog_device *wdd, unsigned int t) { // 更新超时时间 wdd->timeout = t; // 喂一次狗, 实际上在 watchdog_dev.c 里面调用此回调之后 // 还会再调用一次 .ping 进行喂狗一次, 所以这里可有可无 return gpio_wdt_ping(wdd); } // 配置此看门狗支持的操作 static const struct watchdog_info gpio_wdt_ident = { // WDIOF_MAGICCLOSE: 支持看门狗被应用层关闭 // WDIOF_KEEPALIVEPING: 支持应用层喂狗 // WDIOF_SETTIMEOUT: 支持应用层设置超时时间 .options = WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT, .identity = "GPIO Watchdog", }; // 看门狗对接上层应用的回调接口 static const struct watchdog_ops gpio_wdt_ops = { .owner = THIS_MODULE, .start = gpio_wdt_start, // 启动看门狗,对应应用层 WDIOC_SETOPTIONS->WDIOS_ENABLECARD .stop = gpio_wdt_stop, // 关闭看门狗,对应应用层 WDIOC_SETOPTIONS->WDIOS_DISABLECARD .ping = gpio_wdt_ping, // 进行喂狗操作,对应应用层 WDIOC_KEEPALIVE .set_timeout = gpio_wdt_set_timeout, // 设置看门狗超时时间, 设置的同时也会喂一次狗, 对应应用层 WDIOC_SETTIMEOUT }; // 匹配上平台设备, 则会被执行 static int gpio_wdt_probe(struct platform_device *pdev) { struct gpio_wdt_priv *priv; struct gpio_config gpio_flags; unsigned int hw_margin; unsigned long f = 0; const char *algo; int ret; // 分配空间,存储私有数据 priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; // 作为私有数据放入到平台设备 platform_set_drvdata(pdev, priv); // 读取连接看门狗芯片 WDI 引脚的 gpio priv->gpio = of_get_gpio_flags(pdev->dev.of_node, 0, (enum of_gpio_flags *)&gpio_flags); if (!gpio_is_valid(priv->gpio)) return priv->gpio; // 根据当前 gpio 的电平状态作为电平有效性依据, 来确定关闭看门狗时该 gpio 的电平 priv->active_low = gpio_flags.data & OF_GPIO_ACTIVE_LOW; // 读取清除看门狗计数的方式 ret = of_property_read_string(pdev->dev.of_node, "hw_algo", &algo); if (ret) return ret; if (!strcmp(algo, "toggle")) { // 高低电平切换方式, 引脚设置为输入, 这样不会误触发看门狗 priv->hw_algo = HW_ALGO_TOGGLE; f = GPIOF_IN; } else if (!strcmp(algo, "level")) { // 高电平或低电平脉冲方式, 并根据电平有效性来关闭喂狗 priv->hw_algo = HW_ALGO_LEVEL; f = priv->active_low ? GPIOF_OUT_INIT_HIGH : GPIOF_OUT_INIT_LOW; } else { return -EINVAL; } // 申请 gpio 并配置初始状态 ret = devm_gpio_request_one(&pdev->dev, priv->gpio, f, dev_name(&pdev->dev)); if (ret) return ret; // 读取看门狗电路会触发复位的时间(毫秒), 该参数决定定时器定时时间 ret = of_property_read_u32(pdev->dev.of_node,"hw_margin_ms", &hw_margin); if (ret) return ret; /* Disallow values lower than 2 and higher than 65535 ms */ // 不能小于2: 安全的喂狗时间取 1/2, 因此不能小于 2 // 不能大于 65535 : 不清楚为什么这么限制,意味着最大不能超过 65.535 秒 if (hw_margin < 2 || hw_margin > 65535) return -EINVAL; // 取超时时间的一半作为喂狗时间比较安全 /* Use safe value (1/2 of real timeout) */ priv->hw_margin = msecs_to_jiffies(hw_margin / 2); // 读取看门狗配置,是否需要一直运行,配置为 TRUE 意味着此看门狗是不能被应用层关闭的 priv->always_running = of_property_read_bool(pdev->dev.of_node, "always-running"); // 作为私有数据放入到看门狗设备 watchdog_set_drvdata(&priv->wdd, priv); // 设置看门狗支持的操作和相应的回调接口, 响应应用层的具体操作, 如启动、停止、设置超时时间等等。 priv->wdd.info = &gpio_wdt_ident; priv->wdd.ops = &gpio_wdt_ops; priv->wdd.min_timeout = SOFT_TIMEOUT_MIN; priv->wdd.max_timeout = SOFT_TIMEOUT_MAX; priv->wdd.parent = &pdev->dev; // 初始化软件喂狗超时时间的配置, 这里会被设置为默认值 60 秒. // 注意, 这里配置的只是应用层多长时间内没有喂狗 if (watchdog_init_timeout(&priv->wdd, 0, &pdev->dev) < 0) priv->wdd.timeout = SOFT_TIMEOUT_DEF; // 安装定时器, 通过定时器来实现给硬件看门狗喂狗 setup_timer(&priv->timer, gpio_wdt_hwping, (unsigned long)&priv->wdd); // 设置重启时关闭看门狗 // 如果应用层启动了看门狗, 则重启时就会触发 stop 接口 // 通过 watchdog_core.c 注册了重启函数调用链 // 当应用层通过 watchdog_dev.c 启动了看门狗,在系统重启时在通知了链里面会触发 stop 接口 watchdog_stop_on_reboot(&priv->wdd); // 作为看门狗设备驱动注册到系统中 ret = watchdog_register_device(&priv->wdd); if (ret) return ret; // 如果需要一直运行, 则启动喂狗操作 if (priv->always_running) gpio_wdt_start_impl(priv); return 0; } // 在驱动程序移除时执行 static int gpio_wdt_remove(struct platform_device *pdev) { struct gpio_wdt_priv *priv = platform_get_drvdata(pdev); // 删除定时器, 不再继续喂狗 del_timer_sync(&priv->timer); // 注销当前看门狗设备驱动 watchdog_unregister_device(&priv->wdd); return 0; } // 匹配 dts 的名字 static const struct of_device_id gpio_wdt_dt_ids[] = { { .compatible = "linux,wdt-gpio", }, { } }; MODULE_DEVICE_TABLE(of, gpio_wdt_dt_ids); static struct platform_driver gpio_wdt_driver = { .driver = { .name = "gpio-wdt", .of_match_table = gpio_wdt_dt_ids, }, .probe = gpio_wdt_probe, .remove = gpio_wdt_remove, }; #ifdef CONFIG_GPIO_WATCHDOG_ARCH_INITCALL static int __init gpio_wdt_init(void) { //注册平台驱动 return platform_driver_register(&gpio_wdt_driver); } arch_initcall(gpio_wdt_init); #else module_platform_driver(gpio_wdt_driver); #endif MODULE_AUTHOR("Alexander Shiyan <shc_work@mail.ru>"); MODULE_DESCRIPTION("GPIO Watchdog"); MODULE_LICENSE("GPL");
-
-
回复: A133 tplayerdemo 播放视频出错。。。
@memory 抱歉,我没用过 tplayerdemo 来播放视频,或许可以通过查阅这部分代码或配置文件可以找到解决方法。
-
回复: Linux4.9 A100 加载 gpio-keys 驱动可以使用,但是装载过程驱动报错,并且无法卸载。
@yuzukitsuru 看来以后得多逛逛论坛了,前段时间我也遇到这个问题,走了弯路,没想到论坛早有补丁发出来了。哈哈。
-
回复: Linux4.9 A100 加载 gpio-keys 驱动可以使用,但是装载过程驱动报错,并且无法卸载。
@tigger 我最近也有遇到相同的问题,不过当时没看到有这个贴。走了点弯路,不过搞清楚原因了。
https://bbs.aw-ol.com/topic/2099/全志平台-gpio-keys-驱动应用和-stack-crash-解决?_=1663384404172
-
R311 Tina usb gadget ncm wrong ndp sign 问题修复
一、模拟网卡简介
在 Linux 通过 usb 模拟网卡时,有四种方式:
- 使用 usb gadget rndis
- 使用 usb gadget ecm
- 使用 usb gadget ncm
- 使用 usb gadget eem
rndis:
是微软公司制定的协议规范, 不过似乎规范不完整, 引起 rndis host 驱动作者的强烈反对。lichee\linux-4.9\drivers\net\usb\rndis_host.cecm:
传输的是纯粹的以太网包,一次USB传输只包含一个以太网帧,因此吞吐量较低,实测在 6MB/s 左右,Windows 的驱动不太好找,Ubuntu 可以直接支持。ncm:
ecm 的改进版本,每个NCM报文可以包含多个以太网帧,这种特性称为报文聚合,即调用一次URB可以发送或者接收多个IP报文,实测在 17MB/s 左右,Windows 自带有驱动(本文在 Win10 平台实测通过),Ubuntu 可以直接支持,本文使用 ncm 方式实现网卡模拟。eem:
了解不多。二、问题现象
启用步骤很简单,全志也有相应的文档介绍,这里只介绍问题以及解决方法。
接入 Windows 之后,启用该网卡,会不停的输出 "Wrong NDP SIGN",并且无法相互 ping 通。
分析发现因为 ncm->ndp_sign 的值等于 0,未被正确赋值。(f_ncm.c)
分析发现 ncm->ndp_sign 只有在设置 CRC 模式的时候才会拷贝 ncm->parser_opts,由此可见 Win10 的 ncm 驱动并未主动配置 CRC 模式引起。
经调试发现 USB_CDC_SET_NTB_FORMAT 会被触发,会设置 ncm->parser_opts,那么如果也顺道一同拷贝 ncm->ndp_sign 应该就能解决问题。
实测确实解决了问题,既然 f_ncm.c 驱动有缺陷,那么很有可能最新的内核已经解决了此问题,查看了最新内核的驱动(v5.18),发现已经调整了 ncm->ndp_sign 顺序,在 switch 语句后面进行赋值,同样也能解决问题。
三、修复补丁
由于 5.9.y 相对于 4.9 版本改动较大,这里发出一个针对 4.9 版本修复此问题的最小改动补丁:
diff --git a/lichee/linux-4.9/drivers/usb/gadget/function/f_ncm.c b/lichee/linux-4.9/drivers/usb/gadget/function/f_ncm.c old mode 100644 new mode 100755 index 639603722..278580b5a --- a/lichee/linux-4.9/drivers/usb/gadget/function/f_ncm.c +++ b/lichee/linux-4.9/drivers/usb/gadget/function/f_ncm.c @@ -828,7 +828,7 @@ static int ncm_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl) default: goto invalid; } - ncm->ndp_sign = ncm->parser_opts->ndp_sign | ndp_hdr_crc; + // ncm->ndp_sign = ncm->parser_opts->ndp_sign | ndp_hdr_crc; value = 0; break; } @@ -846,6 +846,9 @@ static int ncm_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl) w_value, w_index, w_length); } + // lmx: fix ncm_unwrap_ntb() --> ncm->ndp_sign to==0, generate "Wrong NDP SIGN" Bug + ncm->ndp_sign = ncm->parser_opts->ndp_sign | (ncm->is_crc ? NCM_NDP_HDR_CRC : 0); + /* respond with data transfer or status phase? */ if (value >= 0) { DBG(cdev, "ncm req%02x.%02x v%04x i%04x l%d\n",
实测吞吐量:
-
全志平台 gpio-keys 驱动应用和 stack crash 解决
内核配置
内核版本:Linux version 4.9.56
make ARCH=arm64 menuconfig
Device Drivers ---> Input device support ---> [*] Keyboards ---> <*> GPIO Buttons
配置文件
sys_config.fex
;---------------------------------------------------------------------------------- ;gpio-keys parameters ;---------------------------------------------------------------------------------- [gpio-keys] compatible = "gpio-keys"; [gpio-keys/up] linux,code = 103 linux,input-type = 1 gpios = port:PH11<6><default><default><default> [gpio-keys/down] linux,code = 108 linux,input-type = 1 gpios = port:PH08<0><default><default><default> [gpio-keys/enter] linux,code = 28 linux,input-type = 1 gpios = port:PH10<6><default><default><default>
内存越界
日志信息
[ 2.868360] of_get_named_gpiod_flags: parsed 'gpios' property of node '/soc@01c00000/gpio-keys/up[0]' - status (0) [ 2.868366] of_get_gpio_flags button->gpio:235... [ 2.868393] of_get_named_gpiod_flags: parsed 'gpios' property of node '/soc@01c00000/gpio-keys/down[0]' - status (0) [ 2.868396] of_get_gpio_flags button->gpio:232... [ 2.868417] of_get_named_gpiod_flags: parsed 'gpios' property of node '/soc@01c00000/gpio-keys/enter[0]' - status (0) [ 2.868420] of_get_gpio_flags button->gpio:234... [ 2.869024] input: gpio-keys as /devices/platform/soc/gpio-keys/input/input3 [ 2.869369] Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: ffffff8008600ce0 [ 2.869369] [ 2.869380] CPU: 1 PID: 1 Comm: swapper/0 Not tainted 4.9.56 #165 [ 2.869383] Hardware name: sun50iw1 (DT) [ 2.869388] Call trace: [ 2.869409] [<ffffff800808a7d4>] dump_backtrace+0x0/0x22c [ 2.869417] [<ffffff800808aa24>] show_stack+0x24/0x30 [ 2.869430] [<ffffff80083c9a64>] dump_stack+0x8c/0xb0 [ 2.869438] [<ffffff80081a5960>] panic+0x14c/0x298 [ 2.869450] [<ffffff80080a19d0>] print_tainted+0x0/0xa8 [ 2.869463] [<ffffff8008600ce0>] gpio_keys_probe+0x6d0/0x804 [ 2.869474] [<ffffff80084b3e9c>] platform_drv_probe+0x60/0xac [ 2.869481] [<ffffff80084b1a6c>] driver_probe_device+0x1b8/0x3d4 [ 2.869485] SMP: stopping secondary CPUs [ 2.877351] Kernel Offset: disabled [ 2.877354] Memory Limit: none
解决补丁
diff --git a/drivers/input/keyboard/gpio_keys.c b/drivers/input/keyboard/gpio_keys.c old mode 100644 new mode 100755 index 9b8079c..04e1580 --- a/drivers/input/keyboard/gpio_keys.c +++ b/drivers/input/keyboard/gpio_keys.c @@ -32,6 +32,7 @@ #include <linux/of_gpio.h> #include <linux/of_irq.h> #include <linux/spinlock.h> +#include <linux/sunxi-gpio.h> struct gpio_button_data { const struct gpio_keys_button *button; @@ -664,11 +665,11 @@ static void gpio_keys_close(struct input_dev *input) i = 0; for_each_available_child_of_node(node, pp) { - enum of_gpio_flags flags; + struct gpio_config gpio_flags; button = &pdata->buttons[i++]; - button->gpio = of_get_gpio_flags(pp, 0, &flags); + button->gpio = of_get_gpio_flags(pp, 0, (enum of_gpio_flags *)&gpio_flags); if (button->gpio < 0) { error = button->gpio; if (error != -ENOENT) { @@ -679,7 +680,7 @@ static void gpio_keys_close(struct input_dev *input) return ERR_PTR(error); } } else { - button->active_low = flags & OF_GPIO_ACTIVE_LOW; + button->active_low = gpio_flags.data & OF_GPIO_ACTIVE_LOW; } button->irq = irq_of_parse_and_map(pp, 0);
原因分析
- gpio_keys 驱动使用 of_get_gpio_flags() 获取 dts 里面 gpio 配置信息。
- 但是 of_get_gpio_flags() 传入 enum of_gpio_flags 类型来获取配置信息。
- of_get_gpio_flags() 的最终实现由具体的 SOC 厂商实现,这里是全志厂商实现。
- 实现的函数为:drivers/pinctrl/sunxi/pinctrl-sunxi.c --> sunxi_pinctrl_gpio_of_xlate()。
- 在 sunxi_pinctrl_gpio_of_xlate() 却是通过强制转换 struct gpio_config 类型存储 gpio 配置信息。
- enum of_gpio_flags 占用 4 字节,而 struct gpio_config 占用 20 字节,出现内存越界操作的问题。
// include/linux/of_gpio.h enum of_gpio_flags { OF_GPIO_ACTIVE_LOW = 0x1, OF_GPIO_SINGLE_ENDED = 0x2, }; // 4Byte // include/linux/sunxi-gpio.h struct gpio_config { u32 gpio; u32 mul_sel; u32 pull; u32 drv_level; u32 data; }; // 20Byte // drivers/pinctrl/sunxi/pinctrl-sunxi.c static int sunxi_pinctrl_gpio_of_xlate(struct gpio_chip *gc, const struct of_phandle_args *gpiospec, u32 *flags) { struct gpio_config *config; int pin, base; base = PINS_PER_BANK * gpiospec->args[0]; pin = base + gpiospec->args[1]; pin = pin - gc->base; if (pin > gc->ngpio) return -EINVAL; if (flags) { // 问题出在这个条件下面的赋值语句 // 传进来的是 enum of_gpio_flags,只有 4Byte // 结果使用的 struct gpio_config,却有 20Byte config = (struct gpio_config *)flags; config->gpio = base + gpiospec->args[1]; config->mul_sel = gpiospec->args[2]; config->drv_level = gpiospec->args[3]; config->pull = gpiospec->args[4]; config->data = gpiospec->args[5]; } return pin; }
-
回复: 【入门必看】全志V853开发板——构建编译与固件烧篇
还有一个更加方便的配置编译命令:
- source build/envsetup.sh
- lunch 1
- make && pack
不是很明白为啥楼主要用 ./build.sh 的方式配置编译,就好像不明白我自己刚拿到 SDK 的时候为啥也用 ./build.sh 的方式编译,哈哈。
-
回复: 【V853开发板试用】使用pack命令报错:ERROR: Unsupport PACK_PLATFORM: tinyos
最方便的编译方法是:
- source build/envsetup.sh
- lunch 1
你报错,大概是通过 build.sh 编译,在linux_dev 选择了 tinyos,其实你如果选择 openWRT 也是也可以的。
-
【V853开发板试用】tina make clean unary operator expected 问题
想要清理编译工程的时候,发现清理一半就报错中断了。
修改如下解决:
报错的原因是因为 SKIP_BR 是空的
查询给 SKIP_BR 赋值的地方是 mk_config 这个 function 函数,但这个函数实际未被执行:
我还不是很了解这部分内容,有谁知道的,还请告知下,非常感谢。
-
回复: 【V853 Tina5.0】一个老坑:如果make -j32编译报错那就make -j1单线程编译试试,说不定就编过了
这个是 Tina 的通病吧。可不止 v853
-
回复: YuzukiXR806 使用 FreeRTOS 开发运行Hello Demo
奇怪,我下载下来,配好环境,make menuconfig 会提示缺少 <ncurses.h> 文件,为啥楼主那么顺利的 :
$ make menuconfig make[1]: Entering directory '/cygdrive/d/Work/xr806_sdk/tools/config' HOSTCC mconf.c In file included from mconf.c:23: lxdialog/dialog.h:19:10: fatal error: ncurses.h: No such file or directory 19 | #include <ncurses.h> | ^~~~~~~~~~~ compilation terminated. make[1]: *** [Makefile:24: build/mconf.o] Error 1 make[1]: Leaving directory '/cygdrive/d/Work/xr806_sdk/tools/config' /cygdrive/d/Work/xr806_sdk tools/config/mconf ./Kconfig make: tools/config/mconf: No such file or directory make: *** [Makefile:124: menuconfig] Error 127
-
回复: R11的I2S没有MCLK引脚, 如果要驱动声卡,应该怎么处理呢?可以用一个24.576MHz有源晶振代替吗?
MCLK 是用来同步多颗芯片时钟的,如果只是单颗芯片,压根就不需要 MCLK 。
-
回复: 求教:关于Tina linux SDK代码架构问题
Linux 和 uboot 可以使用 PF_Prj_Gen.sh(开源的第三方脚本),进行过滤一下,这样可以大大减少加入 source insight 工程的文件。其中一个原理是通过寻找 .o 中间文件,然后找对应的 .c 文件,记录到 txt 文档中,然后 source insight 导入这 txt ,建立工程。
-
回复: D1哪吒开发板wifi连接出错,这是什么情况?
@ubuntu 这种通常都是 WIFI 驱动存在问题,如加载的不是对应芯片的驱动,又如驱动识别不到芯片(通讯异常)。具体要看机器启动时的内核打印信息。