[{"content":"vendor/giec/ 厂商定制层分析 基于 device-giec.mk 逐行追踪，理解\u0026quot;厂商如何改系统\u0026quot; 参考代码库：vendor/giec/，Android 14 + Amlogic S905X5M\n目录 device-giec.mk 总览 GApps 集成（条件编译） SELinux 策略 HIDL 服务声明（manifest + matrix） HIDL 客户端与服务端（hwstbcmdservice） 预置 APK 预置二进制与 Shell 脚本 设备配置与条件编译 ADB 与调试配置 签名配置 1. device-giec.mk 总览 vendor/giec/ ├── Android.mk # 入口：遍历所有 subdir ├── device-giec.mk # ★ 核心 Makefile，被 device 层 include ├── common/ │ ├── Android.mk # 编译 apps/ 和 libraries/ 下的模块 │ ├── Android.bp │ ├── sepolicy/ # SELinux 规则（.te 文件） │ ├── manifest.xml # HIDL 服务声明（设备提供什么服务） │ ├── compatibility_matrix.xml # HIDL 需求声明（设备需要什么服务） │ ├── keys/ │ │ └── adbkey.pub # ADB 预置公钥 │ ├── libraries/hwstbcmdapi/ # ★ 核心 HIDL 服务完整实现 │ └── files/Privacy_Policy.txt # 隐私政策 ├── apps/ # 预置 APK │ ├── OTAClient/ │ ├── BazeportLauncher/ │ ├── BazeportSystem/ │ ├── LeanKeyboard/ │ ├── Glauncher/ │ ├── STB-TEST/ │ ├── remove_unused_module/ │ └── busybox/ ├── hardware/interfaces/ # HIDL .hal 定义 │ └── hwstbcmdservice/ │ └── 1.0/ └── executable/ # Shell 脚本工具集 ├── get_display_id.sh └── network/ device-giec.mk 在构建系统中被 device/amlogic/ross/device.mk（或类似设备配置）通过 $(call inherit-product, vendor/giec/device-giec.mk) 包含。它的作用是把厂商所有的定制内容注入到构建产物中。\n2. GApps 集成（条件编译） ifeq ($(BOARD_COMPILE_GAPPS_TV),true) $(call inherit-product, vendor/gapps_tv/arm/arm-vendor.mk) endif 通过环境变量 BOARD_COMPILE_GAPPS_TV=true 控制是否集成 Google 移动服务（Android TV 版） GApps 放在独立的 vendor/gapps_tv/ 目录，与厂商定制代码解耦 这是 Android 构建系统典型的条件编译模式 3. SELinux 策略 SELinux (Security-Enhanced Linux) 是 Linux 内核的强制访问控制（MAC） 系统，由 NSA 开发。与传统的\u0026quot;用户/组/其他\u0026quot;权限（DAC，自主访问控制）不同，SELinux 对所有进程和资源的访问都通过安全策略进行强制检查。\nAndroid 从 5.0 起强制开启 SELinux（Enforcing 模式）。它的核心作用：即使一个进程被提权攻击，SELinux 策略也会阻止它访问未授权的资源。在 Android 嵌入式系统中，厂商需要为每个新增的守护进程编写 SELinux 策略。\nBOARD_SEPOLICY_DIRS += vendor/giec/common/sepolicy 此行把厂商的 sepolicy 目录加入编译。目录中的 .te 文件会被编译进 bootimage 中的 sepolicy 二进制。\n在参考代码库中的体现：厂商为自定义的 hwstbcmdservice HAL 服务单独定义了一个 SELinux 域 hal_hwstbcmdservice，并为 system_app 和 platform_app 授予了调用该服务的权限。\n3.1 文件清单 文件 用途 hal_hwstbcmdservice.te ★ 核心域：为 hwstbcmdservice HAL 进程定义的 SELinux 域（65KB，超详细） system_app.te 允许 system_app 和 platform_app 找到并使用 hwstbcmdservice HIDL 服务 adbd.te 允许 adbd 读取 /vendor/etc/adb/ 下的预置 ADB 密钥 avc.te 解决 avc denials：补充 hal_hwstbcmdservice 对 device 和 vndbinder 的访问 hal_wifi_default.te 允许 WiFi HAL 读调试属性 hal_wifi_hostapd_default.te 允许 WiFi Hostapd HAL 读 sysfs 和 vendor 属性 permissive.te 将 hal_hwstbcmdservice 设为 permissive 模式 vendor_property.te 定义厂商自定义属性的类型和访问规则 file_contexts 为 HAL 二进制和 ADB 密钥目录分配安全上下文 property_contexts 为厂商自定属性（蓝牙、WiFi、自动连接前缀）绑定上下文 hwservice_contexts 为 HIDL 服务接口绑定安全上下文 hwservice.te 声明 HIDL 服务接口类型 3.2 hal_hwstbcmdservice 域（核心） # 声明域和可执行文件类型 type hal_hwstbcmdservice, domain; type hal_hwstbcmdservice_exec, exec_type, vendor_file_type, file_type; # 使用 hwbinder 通信 hwbinder_use(hal_hwstbcmdservice); # 声明为服务管理器可管理的服务类型 type hwstbcmdservice_1_0_service, service_manager_type; # init 进程启动此守护进程 init_daemon_domain(hal_hwstbcmdservice) # 注册 HIDL 服务到 hwservicemanager add_hwservice(hal_hwstbcmdservice, vnd_hwstbcmdservice_hwservice) # 设备和文件访问（精简示例） allow hal_hwstbcmdservice device:chr_file { ioctl }; allow hal_hwstbcmdservice proc:file { read write open getattr }; allow hal_hwstbcmdservice sysfs:file { open write read }; 这个 .te 文件有 300 行，几乎是把所有可能需要的权限都加上了（包括很多被注释掉的）。它承担了大量功能：\n执行 shell 命令（popen）、读写 sysfs/proc、操作设备节点 网络操作（udp_socket、tcp_socket、packet_socket） 与 system_app、platform_app 进行 Binder 通信 读日志、写 sdcard、重启系统 3.3 system_app.te —— 客户端访问权限 allow system_app vnd_hwstbcmdservice_hwservice:hwservice_manager { find }; allow system_app hal_hwstbcmdservice:binder { call transfer }; allow platform_app vnd_hwstbcmdservice_hwservice:hwservice_manager { find }; system_app 和 platform_app 需要显式授权才能通过 hwservicemanager 查找（find）并使用（call/transfer）这个 HAL 服务。\n3.4 permissive.te —— 调试阶段的\u0026quot;大赦\u0026quot; Permissive 模式：SELinux 有两种工作模式。Enforcing（强制）模式会阻止违规操作并记录日志，Permissive（宽容）模式只记录日志但不阻止操作。Permissive 通常用于开发和调试阶段，方便快速定位哪些访问被 SELinux 阻止了（在 dmesg \\| grep avc 中查看）。\npermissive hal_hwstbcmdservice; 这行说：hal_hwstbcmdservice 域的所有访问都不被阻止，只记录日志。这是厂商开发阶段的常见做法——先让功能跑起来，再慢慢收紧规则。生产固件中应该去掉这行。\n在参考代码库中的体现：整个 permissive.te 只有这一条规则。结合 hal_hwstbcmdservice.te 长达 300 行的 allow 规则来看，该服务的权限尚未完全收敛，处于\u0026quot;边跑边加\u0026quot;的阶段。\n3.5 属性与上下文映射 property_contexts 定义了厂商自定属性：\nro.vendor.bluetooth.disable → vendor_custom_prop ro.vendor.wifi.disable → vendor_custom_prop ro.vendor.autoconnectbt.nameprefix → vendor_custom_prop persist.vendor.need.btsetup → vendor_custom_prop vendor_property.te 中：\ntype vendor_custom_prop, property_type; get_prop(appdomain, vendor_custom_prop); # 所有应用可读 set_prop(vendor_init, vendor_custom_prop); # 仅 vendor_init 可写 file_contexts 关键映射：\n/vendor/bin/hw/giec.hardware.hwstbcmdservice@1.0-service → hal_hwstbcmdservice_exec /vendor/etc/adb(/.*)? → vendor_configs_file hwservice_contexts 映射 HIDL 接口到安全上下文：\ngiec.hardware.hwstbcmdservice::IHwstbcmdservice → vnd_hwstbcmdservice_hwservice 4. HIDL 服务声明（manifest + matrix） HIDL (HAL Interface Definition Language) 是 Android Treble 架构引入的硬件抽象层接口定义语言。它的核心目的：将 Framework 与硬件厂商的 HAL 实现解耦，使得 Framework 可以独立更新而无需等待芯片厂商适配。\n工作方式：通过 .hal 文件定义接口（类似 C++ 的头文件），然后由 hidl-gen 工具自动生成 C++/Java 的客户端和服务端代码。通信使用 hwbinder（与 Android 应用层的 Binder 不同的 IPC 通道）。\n在参考代码库中的体现：厂商自定义了一个 giec.hardware.hwstbcmdservice@1.0 的 HIDL 服务，定义在 hardware/interfaces/hwstbcmdservice/1.0/IHwstbcmdservice.hal，包含一个执行 shell 命令的接口。这是典型的\u0026quot;通过 HIDL 给应用层暴露底层能力\u0026quot;的模式。\nTreble 架构下，系统中有两个角色文件来管理 HIDL 服务：\nmanifest.xml：设备提供了哪些 HIDL 服务 compatibility_matrix.xml：设备需要 Framework 提供哪些服务 4.1 manifest.xml —— 我提供什么 \u0026lt;manifest version=\u0026#34;1.0\u0026#34; type=\u0026#34;device\u0026#34; target-level=\u0026#34;8\u0026#34;\u0026gt; \u0026lt;hal format=\u0026#34;hidl\u0026#34;\u0026gt; \u0026lt;name\u0026gt;giec.hardware.hwstbcmdservice\u0026lt;/name\u0026gt; \u0026lt;transport\u0026gt;hwbinder\u0026lt;/transport\u0026gt; \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; \u0026lt;interface\u0026gt; \u0026lt;name\u0026gt;IHwstbcmdservice\u0026lt;/name\u0026gt; \u0026lt;instance\u0026gt;default\u0026lt;/instance\u0026gt; \u0026lt;/interface\u0026gt; \u0026lt;/hal\u0026gt; \u0026lt;/manifest\u0026gt; type=\u0026quot;device\u0026quot;：这是一个设备端 manifest，声明设备实际提供的 HIDL 服务 target-level=\u0026quot;8\u0026quot;：对应 Android 14 的 API 级别 transport=\u0026quot;hwbinder\u0026quot;：使用 hwbinder 通信（不是 vsbinder 或 binder） instance=\u0026quot;default\u0026quot;：服务实例名，创建服务时用这个名字注册 4.2 compatibility_matrix.xml —— 我需要什么 \u0026lt;compatibility-matrix version=\u0026#34;1.0\u0026#34; type=\u0026#34;device\u0026#34;\u0026gt; \u0026lt;hal format=\u0026#34;hidl\u0026#34; optional=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;!-- 内容同 manifest，但与 Framework 的 matrix 做匹配检查 --\u0026gt; \u0026lt;/hal\u0026gt; \u0026lt;/compatibility-matrix\u0026gt; type=\u0026quot;device\u0026quot;：设备端的 matrix，声明 Framework 必须提供满足这些需求的服务 optional=\u0026quot;true\u0026quot;：这个服务是可选的，Framework 没有也能工作 作用：Framework 的 compatibility_matrix.xml 和设备的 manifest.xml 做交叉检查 简单理解：manifest = \u0026ldquo;我有什么\u0026rdquo;，matrix = \u0026ldquo;我需要什么\u0026rdquo;。系统启动时 hwservicemanager 根据 manifest 注册服务，Framework 根据 matrix 查找所需服务。\n5. HIDL 客户端与服务端（hwstbcmdservice） 这是这个 vendor 层最核心的代码。完整调用链：\nJava App (ShellCmd.java) → JNI (cn_giec_adp_cmd.cpp) → Client (HiStbcmdserviceManagerClient.cpp) → HIDL IPC (hwbinder) → HAL Service (Hwstbcmdservice.cpp) → Native (StbCmdShellhal.c) → popen() 执行 shell 命令 5.1 HIDL 接口定义 hardware/interfaces/hwstbcmdservice/1.0/IHwstbcmdservice.hal：\npackage giec.hardware.hwstbcmdservice@1.0; interface IHwstbcmdservice { hsInvokeHal(string request, int32_t type) generates(string result); }; 非常简洁：一个函数，输入字符串命令，输出字符串结果。type 参数控制是否等待返回结果（0=不等待，1=等待）。\n5.2 构建配置 hardware/interfaces/Android.bp 中定义了 HIDL package root：\nhidl_package_root { name: \u0026#34;giec.hardware\u0026#34;, path: \u0026#34;vendor/giec/hardware/interfaces\u0026#34;, } cc_defaults { name: \u0026#34;hidl_giec\u0026#34;, cflags: [\u0026#34;-Wall\u0026#34;, \u0026#34;-Werror\u0026#34;], } 1.0/Android.bp 使用 hidl-gen 生成代码：\nhidl_interface { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0\u0026#34;, root: \u0026#34;giec.hardware\u0026#34;, system_ext_specific: true, # 在 system_ext 分区 srcs: [\u0026#34;IHwstbcmdservice.hal\u0026#34;], gen_java: true, # 同时生成 Java 绑定 } default/Android.bp 编译实现和服务：\n# 实现库（.so） cc_library_shared { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0-impl\u0026#34;, srcs: [\u0026#34;Hwstbcmdservice.cpp\u0026#34;], shared_libs: [\u0026#34;libstbcmdservicehal\u0026#34;, \u0026#34;giec.hardware.hwstbcmdservice@1.0\u0026#34;], } # 可执行服务 cc_binary { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0-service\u0026#34;, relative_install_path: \u0026#34;hw\u0026#34;, # → /vendor/bin/hw/ init_rc: [\u0026#34;...service.rc\u0026#34;], shared_libs: [\u0026#34;giec.hardware.hwstbcmdservice@1.0-impl\u0026#34;], } 5.3 hwbinder 与 vndbinder HIDL 服务通信使用 hwbinder，这是 Android 中与普通 Binder 不同的独立 IPC 通道。\nAndroid 有三种 Binder 域：\nBinder 域 设备节点 用途 binder /dev/binder Framework 内部（Java 应用间的 IPC） hwbinder /dev/hwbinder Framework 与 HAL 之间的 HIDL 通信 vndbinder /dev/vndbinder Vendor 进程之间的 Binder 通信 为什么要隔离？ 为了 Treble 架构的\u0026quot;Framework 可独立升级\u0026quot;目标。Framework 只用 binder 和 hwbinder，vendor 进程使用 vndbinder。这样 Framework 更新不会影响 vendor 域的 Binder 协议。\n在参考代码库中的体现：service.cpp 中通过 ProcessState::initWithDriver(\u0026quot;/dev/vndbinder\u0026quot;) 显式指定使用 vndbinder，表明该 HAL 服务属于 vendor 域。\n5.4 HAL 服务实现 Hwstbcmdservice.cpp —— HIDL 接口实现：\nReturn\u0026lt;void\u0026gt; Hwstbcmdservice::hsInvokeHal( const hidl_string\u0026amp; request, int32_t type, hsInvokeHal_cb _hidl_cb) { const char* result = hsInvokeNative(request.c_str(), type); hidl_string cb(result); _hidl_cb(cb); // 通过回调返回结果（HIDL 异步模式的标准做法） return Void(); } service.cpp —— main 函数，注册为系统服务：\nint main() { ProcessState::initWithDriver(\u0026#34;/dev/vndbinder\u0026#34;); // 使用 vndbinder configureRpcThreadpool(1, true); sp\u0026lt;IHwstbcmdservice\u0026gt; service = HIDL_FETCH_IHwstbcmdservice(\u0026#34;default\u0026#34;); service-\u0026gt;registerAsService(\u0026#34;default\u0026#34;); // 注册为 \u0026#34;default\u0026#34; 实例 joinRpcThreadpool(); } 关键点：\n使用 /dev/vndbinder（vendor 域的 Binder 驱动），与 Framework 的 binder 隔离 HIDL_FETCH_IHwstbcmdservice 是 HIDL 的约定入口函数名，按服务名动态加载实现 5.5 Init RC 文件 service hwstbcmdservice-1-0 /vendor/bin/hw/giec.hardware.hwstbcmdservice@1.0-service class hal # 属于 hal 类（与其它 HAL 服务一起启动/停止） user root # 以 root 运行（因为它需要执行 shell 命令） group root system 5.6 底层执行引擎 StbCmdShellhal.c —— 真正的\u0026quot;干活\u0026quot;代码：\nchar* hsInvokeNative(const char* request, int type) { static char reply[BUFFER_MAX]; // 静态缓冲区，4096 字节 int ret = do_system_cmd(request, reply, flag); return reply; } static int do_system_cmd(const char *arg, char *reply, bool isNeedRet) { fpRead = popen(arg, \u0026#34;r\u0026#34;); // 执行 shell 命令 if (!isNeedRet) { pclose(fpRead); // type=0: 不等待结果 return 0; } while (fgets(buf, ..., fpRead) != NULL) { strncat(reply, buf, ...); // type=1: 读取命令输出 } pclose(fpRead); } 本质：这个 HIDL 服务就是一个\u0026quot;受保护的 shell 命令执行器\u0026quot;。App 通过它执行任意 shell 命令，绕过了 Android 应用沙箱的限制。\nlibstbcmdservicehal 的 Android.bp：\ncc_library_shared { name: \u0026#34;libstbcmdservicehal\u0026#34;, srcs: [\u0026#34;StbCmdShellhal.c\u0026#34;], shared_libs: [\u0026#34;liblog\u0026#34;, \u0026#34;libutils\u0026#34;, \u0026#34;libcutils\u0026#34;], } 5.7 客户端库 HiStbcmdserviceManagerClient.cpp —— C++ 客户端：\nchar* HiStbcmdserviceManagerClient::hsInvokeHalClient(char* request, int type) { sp\u0026lt;IHwstbcmdservice\u0026gt; mHal = IHwstbcmdservice::getService(); // 获取服务 auto cbfun = [\u0026amp;](hidl_string strReply) { memcpy(result, strReply.c_str(), strReply.size() + 1); }; mHal-\u0026gt;hsInvokeHal(request, type, cbfun); // 调用 HIDL 方法 return result; } HiStbcmdserviceManagerClient.h —— 类声明：\nnamespace android { class HiStbcmdserviceManagerClient : public RefBase { public: char* hsInvokeHalClient(char* request, int type); }; } 5.8 JNI 桥接 cn_giec_adp_cmd.cpp —— JNI 实现：\nJNIEXPORT jstring JNICALL Java_cn_giec_shellcmd_ShellCmd_hsInvokeJni( JNIEnv *env, jclass clazz, jstring request, jint type) { char* requestStr = jstringToChar(env, request); char* reply = HiStbcmdserviceManagerClient().hsInvokeHalClient(requestStr, type); return env-\u0026gt;NewStringUTF(reply); } 注册方式（传统的 AndroidRuntime::registerNativeMethods）：\nstatic JNINativeMethod gMethods[] = { {\u0026#34;hsInvokeJni\u0026#34;, \u0026#34;(Ljava/lang/String;I)Ljava/lang/String;\u0026#34;, (void *)Java_cn_giec_shellcmd_ShellCmd_hsInvokeJni}, }; jint JNI_OnLoad(JavaVM* vm, void* reserved) { register_android_histbcmdservicemanage_common(env); return JNI_VERSION_1_4; } 5.9 Java API ShellCmd.java —— 暴露给系统应用的接口：\npackage cn.giec.shellcmd; public class ShellCmd { static { System.loadLibrary(\u0026#34;histbcmdservice_jni\u0026#34;); } private static native String hsInvokeJni(String cmd, int type); public static String exec(String cmd, boolean isReturn) { String fullCmd = \u0026#34;PATH=/system/bin:/system/xbin:/vendor/bin:/vendor/xbin \u0026#34; + cmd; return hsInvokeJni(fullCmd, isReturn ? 1 : 0); } public static void exec(String cmd) { exec(cmd, false); } } stbshellcmd 静态 Java 库的构建（Android.bp）：\njava_library_static { name: \u0026#34;stbshellcmd\u0026#34;, srcs: [\u0026#34;java/**/*.java\u0026#34;], platform_apis: true, # 使用系统 API } 5.10 完整调用链图 ┌─────────────────────────────────────────────────────────────────┐ │ Java 系统应用 │ │ ShellCmd.exec(\u0026#34;ifconfig eth0 down\u0026#34;) │ └──────────────────────────┬──────────────────────────────────────┘ │ JNI ┌──────────────────────────▼──────────────────────────────────────┐ │ libhistbcmdservice_jni.so (JNI 桥接) │ │ cn_giec_adp_cmd.cpp │ └──────────────────────────┬──────────────────────────────────────┘ │ C++ 调用 ┌──────────────────────────▼──────────────────────────────────────┐ │ libhistbcmdservicemanageclient.so (HIDL 客户端) │ │ HiStbcmdserviceManagerClient.cpp │ │ IHwstbcmdservice::getService() → hwbinder │ └──────────────────────────┬──────────────────────────────────────┘ │ HIDL IPC (hwbinder) ┌──────────────────────────▼──────────────────────────────────────┐ │ giec.hardware.hwstbcmdservice@1.0-service (HAL 服务进程) │ │ /vendor/bin/hw/giec.hardware.hwstbcmdservice@1.0-service │ │ Hwstbcmdservice.cpp → hsInvokeHal() │ └──────────────────────────┬──────────────────────────────────────┘ │ C 函数调用 ┌──────────────────────────▼──────────────────────────────────────┐ │ libstbcmdservicehal.so (本地命令执行) │ │ StbCmdShellhal.c → hsInvokeNative() → do_system_cmd() │ │ → popen(\u0026#34;ifconfig eth0 down\u0026#34;, \u0026#34;r\u0026#34;) │ └──────────────────────────┬──────────────────────────────────────┘ │ fork+exec ▼ Linux 内核执行 shell 命令 这个模式在嵌入式 Android 中很典型：厂商提供一个\u0026quot;后门\u0026quot;HAL 服务，让系统应用可以执行 shell 命令，绕过 Android 应用沙箱的限制。这是厂商定制 Rom 的常见做法。\n5.11 device-giec.mk 中的引用 # HIDL 客户端库（被 JNI 库链接） PRODUCT_PACKAGES += \\ libhistbcmdservice_jni \\ libhistbcmdservicemanageclient \\ libstbcmdservicehal # HIDL Treble 服务 PRODUCT_PACKAGES += \\ giec.hardware.hwstbcmdservice@1.0 \\ giec.hardware.hwstbcmdservice@1.0-impl \\ giec.hardware.hwstbcmdservice@1.0-service 6. 预置 APK 6.1 列表 PRODUCT_PACKAGES += \\ LeanKeyboard \\ # ATV 定制键盘 OTAClient \\ # OTA 升级客户端 BazeportLauncher \\ # 定制 Launcher（桌面） BazeportSystem \\ # 系统工具 luojHello # 测试/示例应用 6.2 预置 APK 的 Android.mk 模式 简单预置（LeanKeyboard、OTAClient、luojHello）：\nLOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) APPS := $(notdir $(wildcard $(LOCAL_PATH)/*.apk)) APP_NAME := $(basename $(APPS)) LOCAL_MODULE := $(APP_NAME) LOCAL_SRC_FILES := $(APPS) LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform # 用平台 key 签名 LOCAL_PRIVILEGED_MODULE := false include $(BUILD_PREBUILT) 复杂的预置（BazeportLauncher）：\nLOCAL_PRIVILEGED_MODULE := true # 特权应用 LOCAL_SYSTEM_EXT_MODULE := true # 安装在 system_ext 分区 LOCAL_PREBUILT_JNI_LIBS := ... # 携带本地 .so 库 LOCAL_REQUIRED_MODULES += \\ privapp-permissions-bazeport-next.xml \\ default-permissions-bazeport-next.xml 6.3 权限管理（BazeportLauncher 示例） privapp-permissions-bazeport-next.xml —— 特权权限白名单：\n\u0026lt;privapp-permissions package=\u0026#34;com.bazeport.next\u0026#34;\u0026gt; \u0026lt;!-- 系统级权限 --\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.INSTALL_PACKAGES\u0026#34;/\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.REBOOT\u0026#34;/\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.INJECT_EVENTS\u0026#34;/\u0026gt; \u0026lt;!-- ... 共计 60+ 个权限 --\u0026gt; \u0026lt;/privapp-permissions\u0026gt; default-permissions-bazeport-next.xml —— 运行时权限预授权：\n\u0026lt;exception package=\u0026#34;com.bazeport.next\u0026#34;\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.ACCESS_FINE_LOCATION\u0026#34; fixed=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;!-- fixed=\u0026#34;true\u0026#34; 意味着用户不能撤销此权限 --\u0026gt; \u0026lt;/exception\u0026gt; 权限 XML 为什么要单独放在 system_ext/etc/permissions/？ Android 10+ 要求：privapp 权限必须在 system_ext/etc/permissions/ 下声明才能被识别。\n6.4 移除不需要的应用 # 类型为 FAKE，只声明了一串 LOCAL_OVERRIDES_PACKAGES PRODUCT_PACKAGES += remove_unused_module # 移除的这些应用不会出现在最终的系统中 LOCAL_OVERRIDES_PACKAGES += \\ Browser2 ABUpdater RemoteIME DeskClock \\ TVLauncher \\ # 如果用了 Glauncher AtvRemoteService SetupWraithPrebuilt \\ # GApps 中的一些组件 LiveTv TV DroidLogicLiveTv DroidLogicTvInput LOCAL_OVERRIDES_PACKAGES 是 Android 构建系统提供的包替换机制：如果模块 A override 了模块 B，那么构建时 A 会替代 B 被打包进系统。\n7. 预置二进制与 Shell 脚本 7.1 busybox PRODUCT_PACKAGES += busybox Busybox 是嵌入式 Linux 的瑞士军刀。Android TV 的 Shell 脚本需要它提供的命令（如 udhcpc、awk、grep）。\n构建方式（apps/busybox/Android.mk）：\nLOCAL_MODULE := busybox LOCAL_MODULE_CLASS := EXECUTABLES LOCAL_MODULE_PATH := $(TARGET_OUT)/bin # → /system/bin/busybox LOCAL_SRC_FILES_arm := arm/busybox LOCAL_SRC_FILES_arm64 := arm64/busybox # 不同架构预编译 include $(BUILD_PREBUILT) 7.2 Shell 脚本 PRODUCT_COPY_FILES += \\ vendor/giec/executable/get_display_id.sh:vendor/bin/get_display_id.sh \\ vendor/giec/executable/network/setup_station.sh:vendor/bin/setup_station.sh \\ vendor/giec/executable/network/setup_trunk.sh:vendor/bin/setup_trunk.sh \\ vendor/giec/executable/network/setup_bridge.sh:vendor/bin/setup_bridge.sh \\ vendor/giec/executable/network/udhcpc.script:vendor/bin/udhcpc.script \\ vendor/giec/executable/network/udhcpc_vlan.script:vendor/bin/udhcpc_vlan.script \\ vendor/giec/executable/network/get_network_config.sh:vendor/bin/get_network_config.sh \\ vendor/giec/executable/network/cleanup_network.sh:vendor/bin/cleanup_network.sh PRODUCT_COPY_FILES 语法：源路径:目标路径，把文件复制到目标镜像的指定路径。\n网络工具脚本说明 这些脚本实现了三种网络模式的切换：\n脚本 模式 网络拓扑 setup_station.sh station 模式 eth0 直接配置 DHCP/静态 IP setup_bridge.sh bridge 模式 eth0 + ap0 桥接到 br0，统一获取 IP setup_trunk.sh trunk 模式 eth0 创建 VLAN 子接口，与 ap0 桥接 cleanup_network.sh NAT 模式清理 移除 bridge/VLAN，恢复 eth0 get_network_config.sh 输出 JSON 格式的网络状态，供 Kotlin/Java 应用读取。\n7.3 热点默认开启 PRODUCT_PROPERTY_OVERRIDES += persist.vendor.hotspot.enabled=true PRODUCT_PROPERTY_OVERRIDES：在 /product/build.prop 中写入系统属性，覆盖默认值。\n8. 设备配置与条件编译 8.1 串口控制台 BOARD_KERNEL_CMDLINE += console=ttyS0,921600 ifeq ($(RO_BOOT_ENABLE_CONSOLE),true) BOARD_KERNEL_CMDLINE += androidboot.enableconsole=1 endif BOARD_KERNEL_CMDLINE：追加到 kernel 启动命令行 串口 0，波特率 921600 androidboot.enableconsole=1：控制台在 user 版本中也启用（生产环境中默认关闭） 8.2 条件编译模块 # 选择 Launcher ifeq ($(NEED_GLAUNCHER), true) PRODUCT_PACKAGES += Glauncher endif # 工厂测试工具 ifeq ($(NEED_FACTORY_TEST), true) PRODUCT_PACKAGES += STB_TEST endif # WiFi/BT 测试工具 ifeq ($(BUILD_WIFI_BT_TEST_TOOLS),true) $(call inherit-product, vendor/giec/wifi_bt_test_package.mk) endif 这些变量在 build 命令行或 device.mk 中设置：\nexport NEED_GLAUNCHER=true ./mk ross 9. ADB 与调试配置 9.1 ADB 预置密钥 PRODUCT_COPY_FILES += \\ vendor/giec/common/keys/adbkey.pub:vendor/etc/adb/preinstalled_keys 将公钥复制到 /vendor/etc/adb/preinstalled_keys 使用此公钥的主机连接此设备时无需在设备上确认授权 这在产线测试和开发阶段非常有用 9.2 ADBD 默认开启 ifeq ($(DEFAULT_ADBD_ON),true) PRODUCT_DEFAULT_PROPERTY_OVERRIDES += \\ persist.sys.usb.config=adb endif PRODUCT_DEFAULT_PROPERTY_OVERRIDES 与 PRODUCT_PROPERTY_OVERRIDES 区别：\nPRODUCT_DEFAULT_* 只对 userdebug/eng 版本生效 PRODUCT_* 对所有版本（包括 user）生效 10. 签名配置 ifeq ($(USE_RELEASE_KEY),true) PRODUCT_DEFAULT_DEV_CERTIFICATE := $(CERTIFICATE_DIR)/releasekey else CERTIFICATE_DIR := build/make/target/product/security PRODUCT_DEFAULT_DEV_CERTIFICATE := $(CERTIFICATE_DIR)/testkey endif testkey：AOSP 自带的开发用密钥（公开的，不安全） releasekey：厂商自己的私钥（需要保密） 预置 APK 用 LOCAL_CERTIFICATE := platform 签名，这会使用 PRODUCT_DEFAULT_DEV_CERTIFICATE 指定目录下的 platform.x509.pem / platform.pk8 Android 应用签名：Android 要求每个 APK 都必须用证书签名。系统应用签名尤其重要——只有与系统镜像使用相同密钥签名的应用才能获得 system 级别的 UID 和权限。这就是为什么厂商预置 APK 的 Makefile 中都有 LOCAL_CERTIFICATE := platform。\nPlatform 签名的价值：如果 APK 声明了 android:sharedUserId=\u0026quot;android.uid.system\u0026quot; 或请求了系统级权限（如 INSTALL_PACKAGES），它必须使用 platform 密钥签名，否则系统不会授予相应权限。\nAndroid 有四组签名密钥：\n密钥 用途 testkey 普通 APK，默认 platform 系统级 APK（需与 system 共享 UID 的） shared 共享 UID 的 APK media media 相关 APK 总结 device-giec.mk 虽然只有 133 行，但涉及了每个 Android 厂商定制层的核心要素：\nSELinux 策略 ─── 让定制服务能跑起来 HIDL 服务 ─── 提供底层能力的 IPC 通道 预置 APK ─── 厂商自带的系统应用 Shell 脚本 ─── 网络/调试等运行时工具 构建条件 ─── 区分开发/生产/测试版本 签名密钥 ─── 安全与权限控制 核心理念：Treble 架构下，厂商在 /vendor 分区中进行定制，不需要修改 Framework。 真正的厂商改动都在 vendor/giec/ 这个目录中。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/04-%E5%8E%82%E5%95%86%E5%AE%9A%E5%88%B6%E5%B1%82/","summary":"\u003ch1 id=\"vendorgiec-厂商定制层分析\"\u003evendor/giec/ 厂商定制层分析\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e基于 \u003ccode\u003edevice-giec.mk\u003c/code\u003e 逐行追踪，理解\u0026quot;厂商如何改系统\u0026quot;\n参考代码库：\u003ccode\u003evendor/giec/\u003c/code\u003e，Android 14 + Amlogic S905X5M\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"目录\"\u003e目录\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"#1-device-giecmk-%E6%80%BB%E8%A7%88\"\u003edevice-giec.mk 总览\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#2-gapps-%E9%9B%86%E6%88%90%E6%9D%A1%E4%BB%B6%E7%BC%96%E8%AF%91\"\u003eGApps 集成（条件编译）\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#3-selinux-%E7%AD%96%E7%95%A5\"\u003eSELinux 策略\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#4-hidl-%E6%9C%8D%E5%8A%A1%E5%A3%B0%E6%98%8Emanifest--matrix\"\u003eHIDL 服务声明（manifest + matrix）\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#5-hidl-%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8E%E6%9C%8D%E5%8A%A1%E7%AB%AFhwstbcmdservice\"\u003eHIDL 客户端与服务端（hwstbcmdservice）\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#6-%E9%A2%84%E7%BD%AE-apk\"\u003e预置 APK\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#7-%E9%A2%84%E7%BD%AE%E4%BA%8C%E8%BF%9B%E5%88%B6%E4%B8%8E-shell-%E8%84%9A%E6%9C%AC\"\u003e预置二进制与 Shell 脚本\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#8-%E8%AE%BE%E5%A4%87%E9%85%8D%E7%BD%AE%E4%B8%8E%E6%9D%A1%E4%BB%B6%E7%BC%96%E8%AF%91\"\u003e设备配置与条件编译\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#9-adb-%E4%B8%8E%E8%B0%83%E8%AF%95%E9%85%8D%E7%BD%AE\"\u003eADB 与调试配置\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#10-%E7%AD%BE%E5%90%8D%E9%85%8D%E7%BD%AE\"\u003e签名配置\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-device-giecmk-总览\"\u003e1. device-giec.mk 总览\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003evendor/giec/\n├── Android.mk                     # 入口：遍历所有 subdir\n├── device-giec.mk                 # ★ 核心 Makefile，被 device 层 include\n├── common/\n│   ├── Android.mk                 # 编译 apps/ 和 libraries/ 下的模块\n│   ├── Android.bp\n│   ├── sepolicy/                  # SELinux 规则（.te 文件）\n│   ├── manifest.xml               # HIDL 服务声明（设备提供什么服务）\n│   ├── compatibility_matrix.xml   # HIDL 需求声明（设备需要什么服务）\n│   ├── keys/\n│   │   └── adbkey.pub             # ADB 预置公钥\n│   ├── libraries/hwstbcmdapi/     # ★ 核心 HIDL 服务完整实现\n│   └── files/Privacy_Policy.txt   # 隐私政策\n├── apps/                          # 预置 APK\n│   ├── OTAClient/\n│   ├── BazeportLauncher/\n│   ├── BazeportSystem/\n│   ├── LeanKeyboard/\n│   ├── Glauncher/\n│   ├── STB-TEST/\n│   ├── remove_unused_module/\n│   └── busybox/\n├── hardware/interfaces/           # HIDL .hal 定义\n│   └── hwstbcmdservice/\n│       └── 1.0/\n└── executable/                    # Shell 脚本工具集\n    ├── get_display_id.sh\n    └── network/\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003ccode\u003edevice-giec.mk\u003c/code\u003e 在构建系统中被 \u003ccode\u003edevice/amlogic/ross/device.mk\u003c/code\u003e（或类似设备配置）通过 \u003ccode\u003e$(call inherit-product, vendor/giec/device-giec.mk)\u003c/code\u003e 包含。它的作用是\u003cstrong\u003e把厂商所有的定制内容注入到构建产物中\u003c/strong\u003e。\u003c/p\u003e","title":"04-vendor/giec/厂商定制层分析"},{"content":"Java for Android 速查表 目标读者：有 C/C++ 嵌入式背景，需要读懂 Android Framework 和 APK 源码 原则：遇到再查，不用从头啃 Java 教程。本文档只覆盖读代码必须理解的部分。\n1. Java 与 C/C++ 的核心差异 C/C++ Java 说明 .h + .c / .cpp .java 一个文件一个 public 类，文件名必须和类名一致 #include import import 只是告诉编译器去哪找类，不复制代码 指针 * 无指针 所有对象都通过引用访问，没有 * 和 -\u0026gt; 手动 malloc/free GC 自动回收 new 分配，JVM 自动回收，不用 free struct 用 class 代替 Java 没有 struct，纯数据用 POJO 全局函数 所有函数必须在类里 没有全局函数，static 方法近似 #define / const static final public static final int MAX = 100; bool boolean 注意拼写不一样 NULL null 全小写 2. 类、继承、接口 —— 重点 2.1 类 (Class) 定义方式和 C++ 类似，但区别：所有方法默认是虚函数（可被重写）。\npublic class MyClass { // 成员变量 private int count; public String name; // 构造方法（名和类名相同，没有返回值） public MyClass(String name) { this.name = name; // this 和 C++ 一样 count = 0; } // 方法 public void doSomething() { count++; } } 2.2 继承 (Inheritance) —— Framework 随处可见 // Framework 中最常见的写法 public class ResolutionPropSetterService extends Service { // ... } 要点：\n单继承：一个类只能 extend 一个父类（对比 C++ 可以多继承） 子类自动获得父类的 public / protected 方法和变量 调用父类方法用 super.methodName()（对比 C++ 的 Parent::methodName()） 方法默认是虚函数，子类可以 @Override 2.3 接口 (Interface) —— 理解回调的关键 接口是 纯抽象 的，只定义方法签名，不提供实现：\n// 定义接口（类似 C++ 的纯虚类） public interface OnClickListener { void onClick(View v); } // 实现接口 public class MyActivity extends Activity implements OnClickListener { @Override public void onClick(View v) { // 实现接口方法 } } Framework 中接口的常见场景：回调、Listener、Callback\n// 匿名内部类实现接口（Java 里非常常见的写法） button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 点击后的处理 } }); 2.4 抽象类 (Abstract Class) 介于接口和普通类之间，可以有已实现的方法，也可以有抽象方法。\npublic abstract class BaseTvService { // 抽象方法：子类必须实现 public abstract void onTune(int channel); // 已实现方法：子类可以直接用或 override public void onStop() { // default implementation } } 什么时候是接口，什么时候是抽象类？ 接口定义\u0026quot;能做什么\u0026quot;，抽象类提供\u0026quot;部分默认实现\u0026quot;。Framework 中大量使用接口进行 IPC 回调。\n3. package / import 机制 3.1 package package cn.giec.shellcmd; 对应文件系统路径 cn/giec/shellcmd/ 全限定名：cn.giec.shellcmd.ShellCmd 使用 package 避免类名冲突（类似 C++ 的 namespace） 3.2 import // 导入单个类 import android.app.Service; import android.content.Intent; // 导入整个包（不推荐在 Android 中使用，降低可读性） import android.content.*; // 静态导入（导入静态方法/常量） import static android.Manifest.permission.INTERACT_ACROSS_USERS; 不用记 import：IDE 会自动补全。每次读代码时看到 import 能理解即可。\n4. 异常处理 C 中错误通常靠返回值（-1 / NULL），Java 用 try-catch。\n4.1 基本写法 try { // 可能抛出异常的代码 SystemProperties.set(key, value); } catch (RuntimeException e) { // 处理异常 Slog.e(TAG, \u0026#34;Failed: \u0026#34; + key, e); } 4.2 throws —— 声明方法可能抛异常 // 方法声明时不处理，交给调用者处理 public void readFile(String path) throws IOException { // ... } 4.3 finally —— 无论如何都执行 try { // ... } catch (Exception e) { // ... } finally { // 不管有没有异常，都会执行（类似 RAII 的析构） close(); } 4.4 Framework 中的异常处理风格 参考代码中常见模式：catch 宽泛的异常：\n// 像 ResolutionPropSetterService 中这样 try { SystemProperties.set(key, value); } catch (Throwable t) { // Throwable 是最大的异常范畴 Slog.e(TAG, \u0026#34;Failed\u0026#34;, t); } Throwable \u0026gt; Exception \u0026gt; RuntimeException — 越宽越省事，但会掩盖具体问题。\n5. 常用修饰符 —— 读代码必懂 修饰符 含义 对比 C++ public 任何地方可访问 public private 仅类内部可访问 private protected 子类 + 同包可访问 protected static 属于类本身，不属于实例 static final 类：不可继承 / 方法：不可重写 / 变量：常量 final + 变量 = const abstract 类：不能直接实例化 / 方法：没有实现 纯虚函数 @Override 注解（不是修饰符），表示重写父类方法 C++ 没有此概念 6. static 关键字 —— 理解它的多种用途 public class ShellCmd { // 6.1 静态常量 public static final String OK = \u0026#34;success\u0026#34;; // 6.2 静态变量 private static final String TAG = \u0026#34;ShellCmd\u0026#34;; // 6.3 静态方法（可直接通过类名调用） public static String exec(String cmd) { // ... } // 6.4 静态初始化块（类加载时执行一次） static { System.loadLibrary(\u0026#34;histbcmdservice_jni\u0026#34;); } } // 调用静态方法（不需要创建对象） String result = ShellCmd.exec(\u0026#34;ls\u0026#34;); C/C++ 背景理解：static 方法 ≈ 普通函数，static 变量 ≈ 全局变量，但都放在了类的命名空间里。\n7. 泛型 (Generics) —— Framework 中大量使用 // 写法：类名后面跟 \u0026lt;T\u0026gt;，T 是类型占位符 List\u0026lt;String\u0026gt; nameList = new ArrayList\u0026lt;\u0026gt;(); // 元素必须是 String Map\u0026lt;String, Integer\u0026gt; scoreMap = new HashMap\u0026lt;\u0026gt;(); // key=String, value=Integer Framework 常见泛型：\n// Binder 回调（AIDL 定义的服务） public interface DeathRecipient { void binderDied(); } // 各种 List / Map 泛型 List\u0026lt;Intent\u0026gt; intentList; Map\u0026lt;String, IBinder\u0026gt; serviceMap; 理解泛型就够了：List\u0026lt;String\u0026gt; 就是\u0026quot;只能放 String 的列表\u0026quot;，Map\u0026lt;K,V\u0026gt; 就是\u0026quot;K→V 的映射表\u0026quot;。\n8. 注解 (Annotation) —— 理解 Framework 写法的关键 注解以 @ 开头，给编译器/工具提供额外信息。\n// 最常见的：声明重写父类方法 @Override public IBinder onBind(Intent intent) { return null; } // Framework 中的常见注解 @NonNull // 此参数/返回值不能为 null @Nullable // 此参数/返回值可以为 null @CallSuper // 重写此方法时必须调用 super 实现 @IntDef // 限制 int 参数的可选值 @RequiresPermission // 需要申明权限 遇到不认识的就跳过，不影响理解逻辑流程。注解是元数据，不是业务逻辑。\n9. Framework 核心类速查 9.1 Context（上下文） 一句话：Context 是\u0026quot;访问系统资源的入口\u0026quot;。\n获取资源：context.getString(R.string.hello) 启动 Activity：context.startActivity(intent) 获取系统服务：context.getSystemService(Context.WIFI_SERVICE) 9.2 Intent（意图） 显式 Intent：指明要启动哪个类 Intent intent = new Intent(this, MyActivity.class); startActivity(intent); 隐式 Intent：描述要做的事，让系统找对应的组件 Intent intent = new Intent(\u0026#34;com.example.ACTION_DO_SOMETHING\u0026#34;); startActivity(intent); 携带数据 Intent intent = new Intent(this, MyService.class); intent.putExtra(\u0026#34;key\u0026#34;, \u0026#34;value\u0026#34;); // 参考代码中这种写法很常见 String value = intent.getStringExtra(\u0026#34;key\u0026#34;); 9.3 Activity（页面） 相当于 Android 的\u0026quot;主函数入口\u0026quot;，一个屏幕对应一个 Activity。 生命周期：onCreate() → onStart() → onResume() → onPause() → onStop() → onDestroy()\n只需知道：onCreate() 是入口，Framework 代码中常在 onCreate() 中做初始化。\n9.4 Service（后台服务） 和 Activity 同级，但没有 UI 界面 参考代码中的 ResolutionPropSetterService 就是一个 Service 关键方法：onStartCommand()（收到启动命令时调用）、onBind()（绑定服务时调用） 9.5 BroadcastReceiver（广播接收者） 接收系统或应用发出的广播：\n// 常见的系统广播 Intent.ACTION_BOOT_COMPLETED // 开机完成 Intent.ACTION_PACKAGE_ADDED // 安装应用 10. 理解 Framework Java 代码的关键模式 模式 1：Activity 继承 + 生命周期回调 public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 必须先调 super setContentView(R.layout.activity_main); // 设置布局 } } 模式 2：Service + onStartCommand —— 参考代码中有 public class MyService extends Service { @Override public IBinder onBind(Intent intent) { return null; // 不支持绑定 } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 从 intent 取出参数 String data = intent.getStringExtra(\u0026#34;key\u0026#34;); // 处理... stopSelf(startId); // 处理完就停 return START_NOT_STICKY; } } 模式 3：静态方法工具类 —— ShellCmd 模式 // 类似 C 的全局函数集合 public class ShellCmd { static { System.loadLibrary(\u0026#34;foo\u0026#34;); } public static native String hsInvokeJni(String cmd, int type); public static String exec(String cmd) { return hsInvokeJni(cmd, 0); } } 模式 4：匿名内部类做回调 view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 处理点击 } }); Java 8+ 可以简写为 lambda：view.setOnClickListener(v -\u0026gt; { ... });，但 AOSP 很多老代码还是匿名内部类写法。\n11. 速查：从 C/C++ 映射到 Java 的常见写法 C/C++ Java int *arr = malloc(n * sizeof(int)); int[] arr = new int[n]; printf(\u0026quot;x=%d\\n\u0026quot;, x); Slog.d(TAG, \u0026quot;x=\u0026quot; + x); #define MAX 100 public static final int MAX = 100; if (ptr == NULL) if (obj == null) // 单行注释 // 单行注释（完全一样） /* 多行 */ /* 多行 */（完全一样） for (int i = 0; i \u0026lt; n; i++) 完全一样 if/else/while/do-while 完全一样（bool → boolean） // 返回值表示错误 try-catch 处理错误 namespace package #include import 12. 读代码时常见的\u0026quot;看着怪\u0026quot;的写法 // 1. 链式调用（每个方法返回 this） context.getSystemService(Context.WIFI_SERVICE); // 2. 三元运算符 String value = intent != null ? intent.getStringExtra(\u0026#34;key\u0026#34;) : null; // 3. instanceof 判断类型 if (obj instanceof String) { ... } // 4. == 和 equals() 的区别 // == 比较引用（是否同一个对象） // equals() 比较内容（是否相等） String a = new String(\u0026#34;hello\u0026#34;); String b = new String(\u0026#34;hello\u0026#34;); a == b // false（不同对象） a.equals(b) // true（内容相同） // 5. StringBuilder——大量字符串拼接时用 StringBuilder sb = new StringBuilder(); sb.append(\u0026#34;a=\u0026#34;).append(a).append(\u0026#34;, b=\u0026#34;).append(b); String result = sb.toString(); 13. 学习建议 别背语法：把这份速查表放旁边，读代码时遇到不懂的回来查 先用 Java 读代码：当前阶段目标是\u0026quot;读懂\u0026quot;不是\u0026quot;会写\u0026quot; 重点关注的 Java 特性优先级： 高优先级：类/继承/接口、try-catch、package/import、static 中优先级：泛型、注解、匿名内部类 低优先级：反射、Stream API、Lambda（遇到再学） 搭配 IDE：Android Studio 或 VS Code + Java 插件，代码导航（Ctrl+点击跳转定义）+ 自动补全极大降低读 Java 代码的门槛 遇到具体的 Java 语法问题时，可以用搜索快速定位，或者在参考代码库中搜类似用法。速查表的目的不是一次性读完，而是作为参考夹在笔记本里。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/03-java-%E9%80%9F%E6%9F%A5%E8%A1%A8/","summary":"\u003ch1 id=\"java-for-android-速查表\"\u003eJava for Android 速查表\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e目标读者：有 C/C++ 嵌入式背景，需要读懂 Android Framework 和 APK 源码\n原则：\u003cstrong\u003e遇到再查\u003c/strong\u003e，不用从头啃 Java 教程。本文档只覆盖读代码必须理解的部分。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-java-与-cc-的核心差异\"\u003e1. Java 与 C/C++ 的核心差异\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eC/C++\u003c/th\u003e\n          \u003cth\u003eJava\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e.h\u003c/code\u003e + \u003ccode\u003e.c\u003c/code\u003e / \u003ccode\u003e.cpp\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e.java\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e一个文件一个 public 类，文件名必须和类名一致\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e#include\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eimport\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eimport 只是告诉编译器去哪找类，不复制代码\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e指针 \u003ccode\u003e*\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e无指针\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e所有对象都通过引用访问，没有 \u003ccode\u003e*\u003c/code\u003e 和 \u003ccode\u003e-\u0026gt;\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e手动 \u003ccode\u003emalloc/free\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eGC 自动回收\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enew\u003c/code\u003e 分配，JVM 自动回收，不用 free\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003estruct\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e用 \u003ccode\u003eclass\u003c/code\u003e 代替\u003c/td\u003e\n          \u003ctd\u003eJava 没有 struct，纯数据用 POJO\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e全局函数\u003c/td\u003e\n          \u003ctd\u003e所有函数必须在类里\u003c/td\u003e\n          \u003ctd\u003e没有全局函数，static 方法近似\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e#define\u003c/code\u003e / \u003ccode\u003econst\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003estatic final\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003epublic static final int MAX = 100;\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003ebool\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eboolean\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e注意拼写不一样\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eNULL\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enull\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e全小写\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-类继承接口--重点\"\u003e2. 类、继承、接口 —— 重点\u003c/h2\u003e\n\u003ch3 id=\"21-类-class\"\u003e2.1 类 (Class)\u003c/h3\u003e\n\u003cp\u003e定义方式和 C++ 类似，但区别：\u003cstrong\u003e所有方法默认是虚函数\u003c/strong\u003e（可被重写）。\u003c/p\u003e","title":"03-JavaforAndroid速查表"},{"content":"Android 构建系统入门 学习日期: 2026-04-28 参考项目: Amlogic S905X5M (ross) Android 14 TV 前置知识: 了解 Makefile 基本语法\n1. Android 构建系统的演进 Android 构建系统经历了三个阶段：\nBefore 2014: Android.mk (纯 Makefile) ↓ 2014-2019: Android.mk + Android.bp (Soong) 混用 ↓ 2019+: Android.bp (Soong) 为主，逐步淘汰 Android.mk ↓ Future: Bazel 构建 (Google 正在推进) 当前项目的状态：两者混用。参考代码库中 vendor/giec 既有 24 个 Android.mk，也有 8 个 Android.bp。Google 推荐新代码用 Android.bp。\n2. Android.bp (Soong) 语法详解 2.1 基本结构 Android.bp 使用 Blueprint 格式（类似 JSON 的 DSL），每个代码模块定义为 一个模块类型 + 属性块：\n// 最简单的例子: 把一组文件打包成一个文件组 filegroup { name: \u0026#34;giec_android_certificate_directory\u0026#34;, srcs: [ \u0026#34;*.pk8\u0026#34;, \u0026#34;*.pem\u0026#34;, ], } 来源: vendor/giec/android-certs/Android.bp\n2.2 常用模块类型 模块类型 说明 对应什么 cc_library_shared C/C++ 动态库 (.so) libstbcmdservicehal.so cc_library_static C/C++ 静态库 (.a) cc_binary C/C++ 可执行文件 HIDL 服务进程 java_library Java 库 (.jar) android_app Android 应用 APK filegroup 文件分组 \u0026ldquo;把私钥文件打包成一个模块名\u0026rdquo; hidl_package_root HIDL 包根路径声明 cc_defaults 公共编译参数模板 被其他模块引用 vintf_compatibility_matrix VINTF 兼容性矩阵 2.3 实战例 1: C 动态库 // vendor/giec/common/libraries/hwstbcmdapi/hidl_wrapper/Android.bp cc_library_shared { name: \u0026#34;libstbcmdservicehal\u0026#34;, // 模块名, 其他模块通过这个名字引用 proprietary: true, // 专有, 不公开源码 srcs: [\u0026#34;StbCmdShellhal.c\u0026#34;], // 源文件 cflags: [ // 编译 flags \u0026#34;-Wlong-long\u0026#34;, \u0026#34;-Wformat\u0026#34;, \u0026#34;-Wpointer-arith\u0026#34;, ], shared_libs: [ // 链接的动态库 \u0026#34;liblog\u0026#34;, \u0026#34;libutils\u0026#34;, \u0026#34;libcutils\u0026#34;, ], } 编译产物: libstbcmdservicehal.so\n2.4 实战例 2: C++ 动态库 + 可执行文件 // vendor/giec/hardware/interfaces/hwstbcmdservice/1.0/default/Android.bp // 第一部分: 生成 .so 动态库 cc_library_shared { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0-impl\u0026#34;, defaults: [\u0026#34;hidl_giec\u0026#34;], // 引用公共编译参数 vendor: true, // 放在 vendor 分区 proprietary: true, srcs: [ \u0026#34;Hwstbcmdservice.cpp\u0026#34; ], // 源文件 include_dirs: [\u0026#34;...\u0026#34;], // 头文件搜索路径 cflags: [\u0026#34;-Wall\u0026#34;, \u0026#34;-Werror\u0026#34;], shared_libs: [ // 链接库 (非常重要!) \u0026#34;liblog\u0026#34;, \u0026#34;libhardware\u0026#34;, \u0026#34;libhidlbase\u0026#34;, \u0026#34;libutils\u0026#34;, \u0026#34;libstbcmdservicehal\u0026#34;, // 链接上一个 .bp 编译的库 \u0026#34;giec.hardware.hwstbcmdservice@1.0\u0026#34;, ], } // 第二部分: 生成可执行文件 (服务进程) cc_binary { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0-service\u0026#34;, defaults: [\u0026#34;hidl_giec\u0026#34;], relative_install_path: \u0026#34;hw\u0026#34;, // 安装在 vendor/bin/hw/ 下 vendor: true, proprietary: true, srcs: [\u0026#34;service.cpp\u0026#34;], init_rc: [\u0026#34;giec.hardware.hwstbcmdservice@1.0-service.rc\u0026#34;], cflags: [\u0026#34;-Wall\u0026#34;, \u0026#34;-Werror\u0026#34;], shared_libs: [ \u0026#34;liblog\u0026#34;, \u0026#34;libbinder\u0026#34;, \u0026#34;libutils\u0026#34;, \u0026#34;libhidlbase\u0026#34;, \u0026#34;giec.hardware.hwstbcmdservice@1.0\u0026#34;, \u0026#34;giec.hardware.hwstbcmdservice@1.0-impl\u0026#34;, ], } 关键理解:\nname 是模块的唯一标识，其他模块通过 name 引用 defaults 可以引用 cc_defaults 定义的公共参数，避免重复 vendor: true = 编译产物安装到 vendor 分区，vendor: false = 安装到 system 分区 shared_libs 告诉构建系统链接哪些动态库，构建系统会自动处理依赖顺序 2.5 公共参数模板 // vendor/giec/hardware/interfaces/Android.bp cc_defaults { name: \u0026#34;hidl_giec\u0026#34;, cflags: [ \u0026#34;-Wall\u0026#34;, \u0026#34;-Werror\u0026#34;, ], } 其他模块通过 defaults: [\u0026quot;hidl_giec\u0026quot;] 引用，所有模块共享这些编译参数。\n2.6 HIDL 包根路径声明 // vendor/giec/hardware/interfaces/Android.bp hidl_package_root { name: \u0026#34;giec.hardware\u0026#34;, path: \u0026#34;vendor/giec/hardware/interfaces\u0026#34;, } 告诉 hidl-gen 工具：giec.hardware.* 开头的 HIDL 接口定义在这个目录下。\n2.7 子目录包含 // vendor/giec/common/Android.bp optional_subdirs = [\u0026#34;*\u0026#34;] // 包含所有子目录 // vendor/giec/hardware/interfaces/Android.bp subdirs = [\u0026#34;*\u0026#34;] // 包含所有子目录(非可选) 这两行的区别:\noptional_subdirs = [\u0026quot;*\u0026quot;]: 子目录可选, 子目录不存在不会报错 subdirs = [\u0026quot;*\u0026quot;]: 子目录必须存在, 否则报错 3. Android.mk 语法详解 3.1 基本结构 Android.mk 是传统的 Makefile 格式。每个模块的写法是：\nLOCAL_PATH := $(call my-dir) # 获取当前路径 include $(CLEAR_VARS) # 清除上一次的 LOCAL_ 变量 # module settings LOCAL_MODULE := my_module # 模块名 LOCAL_SRC_FILES := src/main.c # 源文件 LOCAL_CFLAGS := -Wall -O2 # C flags LOCAL_SHARED_LIBRARIES := liblog # 链接的动态库 include $(BUILD_SHARED_LIBRARY) # 指定构建类型 最后一句的 BUILD_* 变量决定了模块类型：\nBUILD_* 变量 模块类型 BUILD_SHARED_LIBRARY 动态库 (.so) BUILD_STATIC_LIBRARY 静态库 (.a) BUILD_EXECUTABLE 可执行文件 BUILD_PACKAGE / BUILD_PREBUILT APK 包 BUILD_JAVA_LIBRARY Java 库 3.2 实战例 3: 预置 APK (带平台签名) # vendor/giec/apps/OTAClient/Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # 自动找到目录下的所有 .apk 文件 APPS := $(notdir $(wildcard $(LOCAL_PATH)/*.apk)) APP_NAME := $(basename $(APPS)) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := $(APP_NAME) # 模块名 = APK 文件名 LOCAL_SRC_FILES := $(APPS) # 源文件是预编译的 APK LOCAL_MODULE_CLASS := APPS # 声明这是一个 APK 模块 LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform # ★ 使用平台 key 签名 → 获得系统权限 LOCAL_PRIVILEGED_MODULE := false # 是否特权应用 (放在 priv-app) LOCAL_OPTIONAL_USES_LIBRARIES := androidx.window.extensions androidx.window.sidecar include $(BUILD_PREBUILT) # 预编译规则 关键理解:\nLOCAL_CERTIFICATE := platform — 这是厂商定制的精髓。用 platform key 签名的 APK 运行在 system 进程中，可以获得系统级权限。普通用户安装的 APK 没有这个权限。 常见取值:\nplatform: 平台签名, 系统权限 media: 媒体签名, 可访问媒体资源 shared: 共享签名 PRESIGNED: 保留原始签名 (不重新签名) 3.3 实战例 4: JNI 动态库 # vendor/giec/common/libraries/hwstbcmdapi/jni/Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := libhistbcmdservice_jni # 生成 .so 供 Java 调用 LOCAL_MODULE_TAGS := optional LOCAL_CFLAGS := -DANDROID_NDK LOCAL_MULTILIB := both # 同时编译 32/64 位 LOCAL_SRC_FILES := cn_giec_adp_cmd.cpp # JNI 桥接源文件 LOCAL_LDLIBS := -ldl -llog LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) ... # 头文件路径 LOCAL_SHARED_LIBRARIES := \\ # 链接的系统库 libandroid_runtime \\ libbinder \\ libutils \\ libcutils \\ libhistbcmdservicemanageclient \\ libgui \\ libhwbinder \\ libnativehelper include $(BUILD_SHARED_LIBRARY) 这个 .so 是 Java → C++ 的桥梁。Java 代码通过 JNI 调用到这个库，再由这个库调用底层 C++ HIDL 服务。\n3.4 目录包含机制 # vendor/giec/Android.mk include $(call all-subdir-makefiles) # vendor/giec/executable/Android.mk include $(all-subdir-makefiles) $(call all-subdir-makefiles): 递归包含所有子目录中的 Android.mk include $(all-subdir-makefiles): 包含同一层所有子目录的 Android.mk 这就是为什么只要在根目录加一个 Android.mk，整个目录树的模块都会被编译。\n4. 构建系统的核心流程 4.1 从 build.sh 开始 参考项目的构建入口 build.sh:\n# 简化后的调用链 ./build.sh -acukoj200 ↓ build.cfg 读取配置 (PRODUCT_NAME, TYPE, ...) ↓ source build/envsetup.sh # 设置环境变量 ↓ lunch ross-userdebug # 选择 target 产品 ↓ make -j200 # 调用 make, 读取 build/make/core/main.mk ↓ 递归扫描所有 Android.mk / Android.bp ↓ 按依赖关系编译所有模块 ↓ 打包成各分区镜像 (system.img, vendor.img, boot.img, ...) ↓ 生成 OTA 包 (如果 -o 指定) 4.2 PRODUCT_PACKAGES 机制 # 在 device.mk 或 device-giec.mk 中: PRODUCT_PACKAGES += \\ LeanKeyboard \\ # → 找 name = \u0026#34;LeanKeyboard\u0026#34; 的模块 OTAClient \\ # → 找 name = \u0026#34;OTAClient\u0026#34; 的模块 BazeportLauncher \\ # → 找 name = \u0026#34;BazeportLauncher\u0026#34; 的模块 libstbcmdservicehal # → 找 name = \u0026#34;libstbcmdservicehal\u0026#34; 的模块 PRODUCT_PACKAGES 是产品清单。构建系统会在所有 Android.mk/Android.bp 中搜索 name 匹配的模块，编译并打包进镜像。\n不写在 PRODUCT_PACKAGES 中的模块不会被编译，即使 Android.mk 文件存在。这是理解 Android 构建的关键。\n4.3 文件拷贝机制 另一种常用的方式是把文件直接拷贝到镜像中：\n# device/amlogic/ross/device.mk PRODUCT_COPY_FILES += \\ device/amlogic/ross/init.amlogic.board.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw/init.amlogic.board.rc 格式: 源文件:目标路径(相对分区根目录)\n$(TARGET_COPY_OUT_VENDOR) = vendor，所以目标路径是 vendor/etc/init/hw/init.amlogic.board.rc\n5. 分区与模块安装位置 Android.bp 中的属性决定模块安装到哪个分区：\nAndroid.bp 属性 安装分区 对应的 make 变量 vendor: true /vendor/ $(TARGET_COPY_OUT_VENDOR) product_specific: true /product/ $(TARGET_COPY_OUT_PRODUCT) system_ext_specific: true /system_ext/ $(TARGET_COPY_OUT_SYSTEM_EXT) 默认 /system/ // 示例: 不同分区的模块 cc_binary { name: \u0026#34;vendor_bin\u0026#34;, vendor: true, # → /vendor/bin/ } cc_binary { name: \u0026#34;system_bin\u0026#34;, # 没有 vendor/product_specific/system_ext_specific # → /system/bin/ (默认) } cc_binary { name: \u0026#34;product_bin\u0026#34;, product_specific: true, # → /product/bin/ } 6. Android.mk vs Android.bp 对比 对比项 Android.mk Android.bp 语法 Makefile Blueprint (JSON-like) 条件语句 支持 (ifeq/ifneq) ❌ 不支持 变量 随意定义 ❌ 不支持自定义变量 通配符 支持 (wildcard) ❌ 必须显式列出 推荐度 旧项目, 正在淘汰 新项目首选 何时用 需要条件判断 模块配置固定 经验法则: 如果不需要 ifeq 做条件判断，用 Android.bp。需要根据变量决定编译内容，用 Android.mk。\n当前项目中两种混用的原因：\nvendor/giec/apps/OTAClient/Android.mk — 因为它要自动通配 APK 文件 vendor/giec/hardware/interfaces/Android.bp — 因为 HIDL 是新特性，用新语法 7. 核心概念总结 ┌─────────────────────────────────────────────────────┐ │ build.sh │ │ 读取 build.cfg → 设置环境 → 调用 make │ └──────────────────────┬──────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ build/make/core/main.mk │ │ Android 构建系统入口, 加载所有 mk/bp 文件 │ └──────────────────────┬──────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ Android.mk / Android.bp 扫描 │ │ 每个模块定义: name, srcs, 依赖, 安装位置 │ └──────────────────────┬──────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ PRODUCT_PACKAGES 筛选 │ │ 只有列在 PRODUCT_PACKAGES 中的模块才被编译 │ └──────────────────────┬──────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 依赖分析 → 编译 → 链接 → 打包 → 生成镜像 │ └─────────────────────────────────────────────────────┘ 8. 从构建到镜像：模块、文件与分区组装 构建系统的最终产物是各个分区镜像。理解镜像如何组装，需要区分三类来源：\n8.1 镜像内容的三大来源 ┌──────────────────────┐ Android.bp/Android.mk │ Modules (模块) │ ← 编译出来的 .so/.apk/可执行文件 PRODUCT_PACKAGES += │ cc_binary, android_app│ └──────────┬───────────┘ ↓ ┌──────────────────────┐ PRODUCT_COPY_FILES │ RAW Files (文件拷贝) │ ← 直接拷贝的配置文件/脚本 / PRODUCCT_COPY_FILES │ .rc, .xml, .cfg, .sh │ └──────────┬───────────┘ ↓ ┌──────────────────────┐ BoardConfig.mk │ Image Config (镜像配置)│ ← 文件系统类型、大小、签名 device.mk │ erofs, ext4, f2fs │ └──────────────────────┘ ↓ ┌──────────────────────┐ │ Final Image │ │ system.img / vendor.img / boot.img ... │ └──────────────────────┘ 来源 1 — 编译出来的模块 (Modules):\nAndroid.bp/Android.mk 定义模块 → PRODUCT_PACKAGES 选中 → 编译 → 安装到对应分区\n# device-giec.mk 中: 这些模块会被编译并装入 system 或 vendor 分区 PRODUCT_PACKAGES += \\ LeanKeyboard \\ # APK → system/system_ext/app/ libstbcmdservicehal \\ # .so → vendor/lib64/ giec.hardware.hwstbcmdservice@1.0-service # 可执行文件 → vendor/bin/hw/ 来源 2 — 直接拷贝的文件 (File Copy):\n不需要编译，直接把源文件按目标路径打包进镜像\n# device/amlogic/ross/device.mk 中的例子: PRODUCT_COPY_FILES += \\ # 配置文件 → vendor/etc/ device/amlogic/ross/files/mixer_paths.xml:$(TARGET_COPY_OUT_VENDOR)/etc/mixer_paths.xml \\ device/amlogic/ross/files/mesondisplay.cfg:$(TARGET_COPY_OUT_VENDOR)/etc/mesondisplay.cfg \\ # init 启动脚本 → vendor/etc/init/hw/ device/amlogic/ross/init.amlogic.board.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw/init.amlogic.board.rc \\ # shell 脚本 → vendor/bin/ vendor/giec/executable/get_display_id.sh:vendor/bin/get_display_id.sh \\ # adb 公钥 vendor/giec/common/keys/adbkey.pub:vendor/etc/adb/preinstalled_keys # 隐私政策 vendor/giec/files/Privacy_Policy.txt:vendor/etc/Privacy_Policy.txt 路径中的 $(TARGET_COPY_OUT_VENDOR) = vendor，所以目标路径是 vendor/etc/mixer_paths.xml，最终在镜像中对应 /vendor/etc/mixer_paths.xml。\n来源 3 — 镜像配置 (Image Config):\n# BoardConfig.mk 中: BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := erofs # system 分区用 erofs (只读) BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := erofs # vendor 分区用 erofs BOARD_USERDATAIMAGE_FILE_SYSTEM_TYPE := f2fs # userdata 分区用 f2fs (读写) BOARD_ODM_EXTIMAGE_FILE_SYSTEM_TYPE := ext4 # odm_ext 分区用 ext4 BOARD_USERDATAIMAGE_PARTITION_SIZE := 576716800 # userdata 分区大小 (550MB) BOARD_BOOTIMAGE_PARTITION_SIZE := 67108864 # boot 分区大小 (64MB) BOARD_SUPER_PARTITION_SIZE := 3355443200 # super 分区大小 (3.2GB) 8.2 各分区镜像构成详解 以 ross (S905X5M) 为例，完整的分区表如下（来自 device/amlogic/ross/part_table_5_15.txt）:\n// partitionName, startsddr, size, gap, attr bootloader_a, -, 8M, 1M, 0x0001 ← 物理分区 boot_a, -, 64M, 1M, 0x0001 ← 物理分区, 包含 kernel+ramdisk super, -, 3200M, 1M, 0x0001 ← 物理分区, 内部再分割为逻辑分区 userdata, -, -, 1M, 0x1004 ← 剩余所有空间 每个镜像的构成拆解：\n镜像文件 分区 内容由什么决定 主要来源 bootloader.img bootloader_a/b U-Boot 编译产物 bootloader/uboot-repo/ 单独构建 boot.img boot_a/b kernel + ramdisk kernel/ 编译 + BOARD_BOOTIMAGE_PARTITION_SIZE vendor_boot.img vendor_boot_a/b vendor 内核模块+DTB vendor_dlkm + DTB dtb.img dtb (分区) 设备树编译产物 ./mk ross -v common14-5.15 构建 dtbo.img dtbo_a/b 设备树 overlay BOARD_DTBOIMG_PARTITION_SIZE super.img super (动态分区) 打包所有动态分区 见下方拆分 ├ system.img (逻辑卷) system 分区内容 TARGET_COPY_OUT_SYSTEM ├ vendor.img (逻辑卷) vendor 分区内容 TARGET_COPY_OUT_VENDOR ├ product.img (逻辑卷) product 分区内容 TARGET_COPY_OUT_PRODUCT ├ system_ext.img (逻辑卷) system_ext 分区内容 TARGET_COPY_OUT_SYSTEM_EXT ├ odm_ext.img (逻辑卷) odm 分区内容 TARGET_COPY_OUT_ODM ├ vendor_dlkm.img (逻辑卷) 可加载内核模块 TARGET_COPY_OUT_VENDOR_DLKM ├ system_dlkm.img (逻辑卷) 系统内核模块 TARGET_COPY_OUT_SYSTEM_DLKM └ odm_dlkm.img (逻辑卷) OEM 内核模块 TARGET_COPY_OUT_ODM_DLKM vbmeta.img vbmeta_a/b AVB 验证元数据 BOARD_AVB_*_ADD_HASHTREE_FOOTER_ARGS userdata.img userdata 空文件系统 首次启动后填充 8.3 以 vendor.img 为例：完整的文件来源追踪 vendor 分区是最复杂的，它同时包含模块产物、拷贝文件、专有二进制：\n模块 (Android.bp/Android.mk → 编译) // .so 动态库 → vendor/lib64/ cc_library_shared { name: \u0026#34;libstbcmdservicehal\u0026#34;, vendor: true, # 装到 vendor 分区的 lib64/ ... } // 可执行文件 → vendor/bin/ cc_binary { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0-service\u0026#34;, vendor: true, relative_install_path: \u0026#34;hw\u0026#34;, # → vendor/bin/hw/ ... } // APK → vendor/app/ 或 vendor/priv-app/ // 如果没有指定 vendor: true, 默认装到 system 分区 文件拷贝 (PRODUCT_COPY_FILES → 直接打包) # 直接拷贝到 vendor 分区的各处: → vendor/etc/mixer_paths.xml # ALSA 音频配置 → vendor/etc/media_codecs.xml # 媒体编解码配置 → vendor/etc/mesondisplay.cfg # 显示配置 → vendor/etc/mali_platform.config # GPU 配置 → vendor/etc/init/hw/init.amlogic.board.rc # 开机启动脚本 → vendor/bin/get_display_id.sh # shell 脚本 → vendor/bin/setup_station.sh # 网络配置脚本 专有二进制 (Prebuilt) # vendor/giec/apps/OTAClient/Android.mk LOCAL_SRC_FILES := $(APPS) # 预编译的 APK, 不是源码 第三方厂商提供的闭源 .so 也属于此类，放在 vendor/amlogic/restricted_libs/ 或 vendor/amlogic/proprietary/。\n8.4 各分区的典型内容速查 分区 典型内容 文件系统 读/写 system Android Framework, 系统应用, 核心库 erofs 只读 system_ext 厂商扩展的系统能力 erofs 只读 vendor BSP 库, HAL 实现, 设备配置, 驱动模块 erofs 只读 product 产品特定配置, 壁纸, 铃声, OTA 配置 erofs 只读 odm OEM 定制 (品牌标识, 差异化配置) ext4 只读(可挂载为rw) vendor_dlkm 可动态加载的内核模块 (.ko) erofs 只读 system_dlkm GKI 兼容的内核模块 erofs 只读 boot kernel + ramdisk raw - vendor_boot vendor 内核模块 + DTB + vendor ramdisk raw - userdata 应用数据, 设置, 用户安装的应用 f2fs 读写 metadata 加密 metadata, 锁屏密码相关 ext4 读写 8.5 总结：一次构建中发生了什么 source build/envsetup.sh → 设置各种 TARGET_COPY_OUT_* 环境变量 → 设置 BOARD_* 分区配置 lunch ross-userdebug → 选择 device/amlogic/ross 的配置 → 加载 BoardConfig.mk (分区大小, 文件系统) → 加载 device.mk (PRODUCT_PACKAGES, PRODUCT_COPY_FILES) make -j200 → Soong 解析所有 Android.bp → Kati 解析所有 Android.mk (转换成 ninja) → Ninja 执行编译 → 编译产物安装到 out/target/product/ross/ → 按分区目录组装: out/.../system/ ← 所有 TARGET_COPY_OUT_SYSTEM 模块+文件 out/.../vendor/ ← 所有 vendor:true 模块+文件 out/.../product/ ← 所有 product_specific 模块+文件 → 打包成镜像: system.img ← out/.../system/ (erofs) vendor.img ← out/.../vendor/ (erofs) super.img ← system.img + vendor.img + product.img + ... (动态分区) boot.img ← kernel + ramdisk (raw) vbmeta.img ← AVB 签名信息 → 生成 OTA 包 (如果 -o 指定) 9. 实操练习 要检验理解，可以尝试以下操作：\n练习 1: 追踪模块（以 libstbcmdservicehal 为例） 下面是对模块 libstbcmdservicehal 的完整追踪，精确到每个文件位置和配置原因。\n文件 1: 模块定义 — Android.bp 文件位置: vendor/giec/common/libraries/hwstbcmdapi/hidl_wrapper/Android.bp\ncc_library_shared { name: \u0026#34;libstbcmdservicehal\u0026#34;, proprietary: true, srcs: [\u0026#34;StbCmdShellhal.c\u0026#34;], cflags: [\u0026#34;...\u0026#34;], shared_libs: [\u0026#34;liblog\u0026#34;, \u0026#34;libutils\u0026#34;, \u0026#34;libcutils\u0026#34;], } 为什么这么配置:\ncc_library_shared → 编译产物是 .so 动态库，供其他模块在运行时链接 proprietary: true → 这是厂商专有代码，不对外公开源码（APACHE 协议的项目不会包含它） srcs: [\u0026quot;StbCmdShellhal.c\u0026quot;] → 源文件在同目录 vendor/giec/common/libraries/hwstbcmdapi/hidl_wrapper/StbCmdShellhal.c shared_libs: [\u0026quot;liblog\u0026quot;, \u0026quot;libutils\u0026quot;, \u0026quot;libcutils\u0026quot;] → 链接 Android 基础库（日志、工具函数、系统裁剪库） 这个模块是干什么的: 查看 StbCmdShellhal.c 的代码，核心逻辑只有两个函数:\ndo_system_cmd() — 调用 popen() 执行 shell 命令 hsInvokeNative() — 对外暴露的接口，接收命令字符串，返回执行结果 简单说：这个库是系统应用执行 shell 命令的桥梁。系统应用通过 JNI 调用到这个库，最终用 popen() 执行命令。\n文件 2: PRODUCT_PACKAGES 引用 — device-giec.mk 文件位置: vendor/giec/device-giec.mk\nPRODUCT_PACKAGES += \\ libhistbcmdservice_jni \\ libhistbcmdservicemanageclient \\ libstbcmdservicehal # ← 在这里 为什么这么配置:\nPRODUCT_PACKAGES 是\u0026quot;产品清单\u0026quot;，只有列在这里的模块才会被编译进固件 这里同时列出了相关的 3 个库，构成了完整的调用链： Java App → libhistbcmdservice_jni (JNI 桥) → libhistbcmdservicemanageclient (客户端管理) → libstbcmdservicehal (实际执行 shell 命令) 文件 3: 依赖引用 — Android.bp 文件位置: vendor/giec/hardware/interfaces/hwstbcmdservice/1.0/default/Android.bp\ncc_library_shared { name: \u0026#34;giec.hardware.hwstbcmdservice@1.0-impl\u0026#34;, ... shared_libs: [ ... \u0026#34;libstbcmdservicehal\u0026#34;, # ← 链接到这个库 \u0026#34;giec.hardware.hwstbcmdservice@1.0\u0026#34;, ], } 为什么这么配置:\nHIDL 服务的实现 (-impl) 需要实际执行 shell 命令的能力，所以链接了 libstbcmdservicehal 这构成了一条完整链路: HIDL Service → libstbcmdservicehal → popen() → shell 命令执行 文件 4-5-6: 包含链（为什么 device-giec.mk 会生效） ross.mk line 228 # 产品主配置 └→ $(call inherit-product, device/amlogic/common/products/mbox/product_mbox.mk) line 1 └→ $(call inherit-product, device/amlogic/common/core_amlogic.mk) line 40 └→ $(call inherit-product-if-exists, vendor/giec/device-giec.mk) line 30 └→ PRODUCT_PACKAGES += libstbcmdservicehal 文件 位置 作用 ross.mk device/amlogic/ross/ross.mk:228 S905X5M 产品入口 product_mbox.mk device/amlogic/common/products/mbox/product_mbox.mk:1 mbox 产品类型通用配置 core_amlogic.mk device/amlogic/common/core_amlogic.mk:40 Amlogic 平台核心配置，触发 GIEC 定制 为什么这样设计:\nAmlogic 把 vendor/giec/device-giec.mk 放在 core_amlogic.mk 中条件包含，意味着只要用的是 Amlogic 平台，厂商定制自动生效 GIEC 只需要维护 vendor/giec/ 目录，不需要修改 Amlogic 的核心配置 文件 7: 模块扫描入口 — Android.mk 文件位置: vendor/giec/Android.mk\ninclude $(call all-subdir-makefiles) 为什么这么配置:\n这一行让构建系统递归扫描 vendor/giec/ 下所有子目录的 Android.mk 和 Android.bp 文件虽然简短，但它构成了模块发现的起点。没有它，libstbcmdservicehal 即使有 Android.bp 也不会被构建系统看到 完整追踪总结 libstbcmdservicehal.so 的完整生命周期 ═══════════════════════════════════════ [模块发现] vendor/giec/Android.mk ← 递归扫描所有子目录 ↓ [模块定义] vendor/giec/common/libraries/hwstbcmdapi/ hidl_wrapper/Android.bp ← name: \u0026#34;libstbcmdservicehal\u0026#34; ↓ cc_library_shared → .so [源文件] hidl_wrapper/StbCmdShellhal.c ← popen() 执行 shell 命令 ↓ [产品清单] vendor/giec/device-giec.mk ← PRODUCT_PACKAGES += libstbcmdservicehal ↓ [包含链] device/amlogic/ross/ross.mk ← 产品入口 → product_mbox.mk ← 产品类型 → core_amlogic.mk ← 核心配置, include device-giec.mk ↓ [编译] 构建系统扫描所有 Android.bp/Android.mk → 匹配 name: \u0026#34;libstbcmdservicehal\u0026#34; → 编译 StbCmdShellhal.c → libstbcmdservicehal.so → 拷贝到 out/target/product/ross/vendor/lib64/ ↓ [运行时链接] giec.hardware.hwstbcmdservice@1.0-service ← HIDL 服务进程 → giec.hardware.hwstbcmdservice@1.0-impl ← 实现库 → libstbcmdservicehal.so ← 实际执行 shell 命令 → popen() → system commands ↓ [JNI 调用] libhistbcmdservice_jni.so ← Java → Native 桥接 → 系统 APK (BazeportSystem 等) ← Java 层调用 练习 2: 理解依赖链（以 OTAClient 为例） 追踪 OTAClient，它不是一个简单的模块，而是涉及构建系统、产品配置、运行时属性、系统应用集成的完整案例。\n引子: OTAClient 是什么 OTAClient 是 GIEC 厂商的定制 OTA 升级客户端。它替代了 AOSP 原生的系统更新功能，与 GIEC 自建的 OTA 服务器通信，完成固件升级包的下载和安装。\n包名: com.giec.otaclient，版本 1.0.5，预编译 APK（无源码）。\n文件 1: 模块定义 / 安装清单 — Android.mk 文件位置: vendor/giec/apps/OTAClient/Android.mk\nLOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) APPS := $(notdir $(wildcard $(LOCAL_PATH)/*.apk)) # 自动找到 OTAClient.apk APP_NAME := $(basename $(APPS)) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := $(APP_NAME) # → 模块名: \u0026#34;OTAClient\u0026#34; LOCAL_SRC_FILES := $(APPS) LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform # ★ 平台签名 → 系统权限 LOCAL_PRIVILEGED_MODULE := false # 普通系统应用，非 priv-app include $(BUILD_PREBUILT) # 预编译 APK 规则 为什么这样配置:\nLOCAL_CERTIFICATE := platform — 用平台 key 签名。这个 APK 需要访问系统级 API（比如调用 Recovery 升级），普通签名没有这个权限 LOCAL_PRIVILEGED_MODULE := false — 安装在 system/app/ 而非 system/priv-app/。priv-app 有更高的权限等级，但 OTAClient 当前的设计不需要 BUILD_PREBUILT — 不是从源码编译，直接打包预编译的 OTAClient.apk 文件 2: PRODUCT_PACKAGES 引用 — device-giec.mk 文件位置: vendor/giec/device-giec.mk\nPRODUCT_PACKAGES += \\ LeanKeyboard \\ OTAClient \\ # ← 列在产品清单中 BazeportLauncher \\ BazeportSystem 为什么: PRODUCT_PACKAGES 是\u0026quot;产品安装清单\u0026quot;，只有列在这里的模块才会被编译进固件。\n文件 3: 包含链（为什么 device-giec.mk 会生效） ross.mk line 228 └→ $(call inherit-product, device/amlogic/common/products/mbox/product_mbox.mk) line 1 └→ $(call inherit-product, device/amlogic/common/core_amlogic.mk) line 40 └→ $(call inherit-product-if-exists, vendor/giec/device-giec.mk) line 47 → PRODUCT_PACKAGES += OTAClient 文件 位置 作用 ross.mk device/amlogic/ross/ross.mk:228 S905X5M 产品入口 mk product_mbox.mk device/amlogic/common/products/mbox/product_mbox.mk:1 mbox 产品类型配置，包含 core_amlogic.mk core_amlogic.mk device/amlogic/common/core_amlogic.mk:40 触发 GIEC 定制: include vendor/giec/device-giec.mk device-giec.mk vendor/giec/device-giec.mk:47 列出 OTAClient 文件 4: OTA 运行时配置 — product_mbox.mk 文件位置: device/amlogic/common/products/mbox/product_mbox.mk\n# OTA update config (line 271-276) PRODUCT_PROPERTY_OVERRIDES += \\ persist.sys.otaurl.ext=$(OTA_UPDATE_REQUEST_URL_EXT) \\ persist.sys.otaurl.int=$(OTA_UPDATE_REQUEST_URL_INT) \\ vendor.build.app.id=$(APP_ID) \\ vendor.build.app.secret=$(APP_SECRET) 为什么这样配置:\n这些是运行时属性，OTAClient 在设备启动后读取这些属性来连接 OTA 服务器 persist.sys.otaurl.ext — OTA 服务器外网地址 persist.sys.otaurl.int — OTA 服务器内网地址 vendor.build.app.id / .secret — 设备身份认证，每个产品型号不同 文件 5: 配置源头 — build.cfg 文件位置: 项目根目录 build.cfg\n# build.cfg 第 75 行 GIEC_BUILD_NUMBER=1.0.00000 # 固件版本号 #OTA 服务器地址（第 104-105 行，默认注释掉） #OTA_UPDATE_REQUEST_URL_EXT=http://14.21.46.164:880/app-api/ota/device/check #OTA_UPDATE_REQUEST_URL_INT=http://14.21.46.164:880/app-api/ota/device/check # 各产品的 APP_ID/APP_SECRET（第 86-102 行） #RAMAN_APP_ID=Kr1Jtb #RAMAN_APP_SECRET=bykGNPARzlFhoG4E #ROSS_APP_ID=lFxCQa #ROSS_APP_SECRET=4FitnLGrsQJkorPg ... 为什么这样配置: build.sh 读取 build.cfg，根据 PRODUCT_NAME 选择对应的 APP_ID/APP_SECRET，然后 export 为环境变量，传递给 make 系统。\n文件 6: 构建时版本注入 — build.sh 文件位置: 项目根目录 build.sh\n# line 5: 版本号文件路径 VERSION_FILE=\u0026#34;/home/workspace/venus/public_repository/.an14_build_version\u0026#34; # line 218: 自动版本号递增逻辑 export GIEC_BUILD_NUMBER=$new_version # line 351-370: 从 build.cfg 读取配置 GIEC_BUILD_NUMBER) ... ;; OTA_UPDATE_REQUEST_URL_EXT) export OTA_UPDATE_REQUEST_URL_EXT=$value ;; OHM_WV4_APP_ID) export APP_ID=$value ;; OHM_WV4_APP_SECRET) export APP_SECRET=$value ;; 文件 7: 系统设置集成 — AboutFragment.java 文件位置: packages/apps/TvSettings/Settings/src/com/android/tv/settings/about/AboutFragment.java:399-412\ncase KEY_SYSTEM_UPDATE_SETTINGS: /* * Due to an update in Google\u0026#39;s GMS, there is a conflict * between GMS and our OTAClient over handling the system update. * This conflict may cause the system update to launch GMS update * instead of our custom OTAClient. * * To resolve this issue, we explicitly set the Intent\u0026#39;s target * component to com.giec.otaclient\u0026#39;s MainActivity. */ Intent systemUpdateIntent = new Intent(); systemUpdateIntent.setComponent( new ComponentName( \u0026#34;com.giec.otaclient\u0026#34;, \u0026#34;com.giec.otaclient.ui.activities.MainActivity\u0026#34; ) ); startActivity(systemUpdateIntent); break; 为什么这样修改: GMS（Google Mobile Services）会抢占系统更新的 intent。厂商通过修改 AOSP 源码（TvSettings），强制将系统更新按钮指向自己的 OTAClient，而不是 Google 的更新组件。\n这表明 GIEC 不仅自己写应用，也修改了 AOSP 原生代码来集成自己的功能。\n完整依赖链总结 OTAClient 的完整生命周期 ════════════════════════════ [配置源头] build.cfg ← GIEC_BUILD_NUMBER, APP_ID/APP_SECRET, OTA URL ↓ [构建脚本读取] build.sh ← 解析 build.cfg → export 环境变量 ↓ [版本管理] .an14_build_version ← 自动维护版本号递增 ↓ [模块定义] vendor/giec/apps/OTAClient/Android.mk ← 定义模块 OTAClient LOCAL_CERTIFICATE := platform → 平台签名 BUILD_PREBUILT → 预编译 APK ↓ [产品清单] vendor/giec/device-giec.mk ← PRODUCT_PACKAGES += OTAClient ↓ [包含链] ross.mk → product_mbox.mk → core_amlogic.mk → device-giec.mk ↓ [编译] 构建系统匹配 OTAClient 模块 → 对 OTAClient.apk 用 platform key 重新签名 → 拷贝到 out/target/product/ross/system/app/OTAClient/ ↓ [OTA 运行时属性注入] product_mbox.mk ← 将 OTA URL、APP_ID、APP_SECRET 写入系统属性 ↓ [设备运行时] OTAClient APK 启动 → 读取 persist.sys.otaurl.ext 获取服务器地址 → 读取 vendor.build.app.id/.secret 进行身份认证 → 请求 OTA 服务器检查更新 → 下载升级包到 /sdcard/Download/ota_gateway_net.zip → 复制到 /data/ota_package/update.zip → 调用 mUpdateEngine 执行升级 ↓ [系统设置入口] TvSettings 的 AboutFragment.java ← GMS 冲突修复 → 强制将\u0026#34;系统更新\u0026#34;按钮指向 com.giec.otaclient 关键对比: OTAClient vs libstbcmdservicehal 维度 OTAClient libstbcmdservicehal 类型 APK (预编译) .so (源码编译) 构建方式 BUILD_PREBUILT cc_library_shared 签名 platform key 无需签名 安装位置 system/app/ vendor/lib64/ 开发语言 Java (预编译) C 功能 OTA 升级 UI + 逻辑 shell 命令执行桥梁 练习 3: 自己写一个 Android.bp 尝试写一个简单的 cc_binary:\ncc_binary { name: \u0026#34;hello_android\u0026#34;, srcs: [\u0026#34;hello.c\u0026#34;], cflags: [\u0026#34;-Wall\u0026#34;], vendor: true, } 把它放到某个目录下，在对应 device.mk 的 PRODUCT_PACKAGES 中加上 hello_android，然后编译验证。\n10. 学习要点速查 Android.bp 的基本结构 (模块类型 + 属性块) Android.mk 的基本结构 (LOCAL_变量 + BUILD_类型) name 是模块的唯一标识，跨文件引用 PRODUCT_PACKAGES 决定哪些模块被编译进镜像 LOCAL_CERTIFICATE := platform 给 APK 系统权限 vendor: true 控制安装分区 cc_defaults 用于共享公共编译参数 shared_libs / LOCAL_SHARED_LIBRARIES 管理依赖 defaults 引用公共参数模板 PRODUCT_COPY_FILES 直接拷贝文件到镜像 镜像内容三来源: 编译模块 + 文件拷贝 + 镜像配置 super 动态分区包含多个逻辑卷 (system/vendor/product/\u0026hellip;) 物理分区 (boot/bootloader/vbmeta) 和动态分区 (super 内) 的区别 11. 下一步 构建系统理解后，建议进入 Phase 2: 厂商定制层，从 vendor/giec/ 的完整目录结构开始，看厂商在一个 AOSP 项目上到底加了哪些东西。\n或者也可以先横向对比练习：在参考代码库中找 3 个不同的模块（APK、so 库、可执行文件），分别用 bp 和 mk 的方式追踪它们的完整构建链路。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/02-android-%E6%9E%84%E5%BB%BA%E7%B3%BB%E7%BB%9F/","summary":"\u003ch1 id=\"android-构建系统入门\"\u003eAndroid 构建系统入门\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e学习日期: 2026-04-28\n参考项目: Amlogic S905X5M (ross) Android 14 TV\n前置知识: 了解 Makefile 基本语法\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-android-构建系统的演进\"\u003e1. Android 构建系统的演进\u003c/h2\u003e\n\u003cp\u003eAndroid 构建系统经历了三个阶段：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eBefore 2014: Android.mk (纯 Makefile)\n     ↓\n2014-2019:   Android.mk + Android.bp (Soong) 混用\n     ↓\n2019+:       Android.bp (Soong) 为主，逐步淘汰 Android.mk\n     ↓\nFuture:      Bazel 构建 (Google 正在推进)\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e当前项目的状态：\u003cstrong\u003e两者混用\u003c/strong\u003e。参考代码库中 vendor/giec 既有 24 个 Android.mk，也有 8 个 Android.bp。Google 推荐新代码用 Android.bp。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-androidbp-soong-语法详解\"\u003e2. Android.bp (Soong) 语法详解\u003c/h2\u003e\n\u003ch3 id=\"21-基本结构\"\u003e2.1 基本结构\u003c/h3\u003e\n\u003cp\u003eAndroid.bp 使用 \u003cstrong\u003eBlueprint\u003c/strong\u003e 格式（类似 JSON 的 DSL），每个代码模块定义为 \u003cstrong\u003e一个模块类型 + 属性块\u003c/strong\u003e：\u003c/p\u003e","title":"02-Android构建系统入门"},{"content":"Android 系统架构总览 学习日期: 2026-04-28 参考项目: Amlogic S905X5M (ross) Android 14 TV 前置知识: Linux embedded C/C++ 基础\n1. Android 分层架构 Android 系统从上到下分为 5 层，每一层都依赖下一层提供的服务：\n┌─────────────────────────────────────────────────┐ │ Applications │ ← 系统应用 + 第三方应用 │ Launcher, Settings, Browser, OTA Client... │ ├─────────────────────────────────────────────────┤ │ Application Framework │ ← 应用框架（Java） │ ActivityManager, WindowManager, PackageManager│ ├─────────────────────────────────────────────────┤ │ Android System Services │ ← 系统服务（Native + Java） │ System Server, SurfaceFlinger, MediaServer... │ ├─────────────────────────────────────────────────┤ │ Hardware Abstraction Layer (HAL) │ ← 硬件抽象层（C/C++） │ Camera HAL, Audio HAL, Graphics HAL... │ ├─────────────────────────────────────────────────┤ │ Linux Kernel │ ← Linux 内核 + 驱动 │ Display Driver, WiFi, BT, Audio, DMA-BUF... │ └─────────────────────────────────────────────────┘ 与传统 Linux embedded 的对比 概念 传统 Linux Android 内核 Linux kernel 同一内核 + Android 特有驱动 (ashmem/binder/ion) init 系统 systemd/busybox init init（专属，解析 .rc 文件） IPC D-Bus/socket Binder（Android 核心 IPC 机制） 设备访问 直接 open /dev/xxx 通过 HAL + HIDL 接口 应用格式 ELF 可执行文件 APK（Java + Native libs） 权限控制 DAC (user/group) DAC + SELinux（强制） 构建系统 Makefile/CMake Soong (Android.bp) + Makefile 2. 各层详解（结合参考代码库） 2.1 Layer 1: Linux Kernel 位置: kernel 源码 + 设备树 DTS\n职责: 硬件驱动、内存管理、进程调度、网络栈\nAndroid 特有的内核特性:\nBinder 驱动: 进程间通信（IPC）的核心 ashmem: 匿名共享内存 ION/DMA-BUF: 内存分配器（图形/多媒体用） wakelocks: 电源管理休眠锁 参考代码库中的体现:\n# BoardConfig.mk 中定义了内核版本和构建方式 # device/amlogic/ross/BoardConfig.mk: TARGET_BOARD_PLATFORM := s6 # 内核通过 ./mk ross -v common14-5.15 构建（ross 的 uboot target: s7d_bm201） # 内核 cmdline 中配置 console、SELinux、分区信息: BOARD_KERNEL_CMDLINE += console=ttyS0,921600 BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive # 条件开启 BOARD_BOOTCONFIG += \u0026#34;androidboot.boot_devices=soc/fe08c000.mmc\u0026#34; 2.2 Layer 2: Hardware Abstraction Layer (HAL) 位置: hardware/、vendor/、device/ 下的 HAL 实现\n职责: 封装内核驱动接口，向上层提供标准化的硬件访问 API\n关键概念 — Treble 架构: Android 8.0 引入 Project Treble，将 HAL 从 Framework 中解耦出来。厂商实现 HAL，Framework 通过 HIDL（HAL Interface Definition Language）调用 HAL 服务。\n通信流程:\nApp/Service ↓ Binder IPC Framework (Java) ↓ JNI Native Service (C++) ↓ HIDL/hwBinder HAL Implementation (C/C++) ← 厂商实现 ↓ system calls Kernel Driver ↓ Hardware 参考代码库中的体现:\n# device/giec/device-giec.mk 中的 HIDL 声明: DEVICE_MANIFEST_FILE += vendor/giec/common/manifest.xml DEVICE_MATRIX_FILE += vendor/giec/common/compatibility_matrix.xml # vendor/giec 的自定义 HAL 服务: PRODUCT_PACKAGES += \\ giec.hardware.hwstbcmdservice@1.0 \\ # HIDL 接口定义 giec.hardware.hwstbcmdservice@1.0-impl \\ # HAL 实现 giec.hardware.hwstbcmdservice@1.0-service # HAL 服务进程 manifest.xml 声明\u0026quot;我这个设备提供了哪些 HAL 服务\u0026quot;，compatibility_matrix.xml 声明\u0026quot;我需要 Framework 给我提供什么\u0026quot;。\nross 设备的外设配置:\n# device/amlogic/ross/BoardConfig.mk USE_OPENGL_RENDERER := true USE_HWC2 := true # 使用 HWC2 (Hardware Composer 2) BOARD_HAVE_FRONT_CAM := false # 无前置摄像头 BOARD_HAVE_BACK_CAM := false # 无后置摄像头 BOARD_USE_USB_CAMERA := true # 使用 USB 摄像头 BOARD_USES_ALSA_AUDIO := true # ALSA 音频架构 2.3 Layer 3: System Services (Native 层) 位置: frameworks/native/、system/core/\n关键进程:\n进程 作用 类比 Linux init PID 1，解析 init.rc 启动所有服务 类似 systemd surfaceflinger 图形合成显示 类似 Weston/Wayland compositor mediaserver 多媒体编解码、播放 类似 GStreamer 管道 servicemanager Binder 服务注册与发现 类似 D-Bus daemon logd 系统日志 类似 syslogd init 启动流程:\nBoot ROM → Bootloader → Kernel → init → init.rc → Zygote → System Server ↓ 所有 Java 系统服务 参考代码库:\n# device/amlogic/ross/device.mk # init 启动脚本: PRODUCT_COPY_FILES += \\ device/amlogic/ross/init.amlogic.board.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw/init.amlogic.board.rc 2.4 Layer 4: Application Framework (Java 层) 位置: frameworks/base/\n核心服务（运行在 System Server 进程中）:\n服务 作用 ActivityManagerService (AMS) 管理应用生命周期、任务栈 WindowManagerService (WMS) 窗口管理、触摸事件分发 PackageManagerService (PMS) APK 安装、权限管理 PowerManagerService 电源管理、休眠策略 NotificationManagerService 通知管理 App 通过 Binder IPC 调用这些服务，不需要知道底层实现。\n2.5 Layer 5: Applications 位置: packages/、vendor/giec/apps/\n分为两类：\n系统应用: 预置在 system 分区，有系统权限 用户应用: 用户安装，权限受限 参考代码库中的系统应用:\n# vendor/giec/device-giec.mk PRODUCT_PACKAGES += \\ LeanKeyboard \\ # 定制输入法 OTAClient \\ # OTA 升级客户端 BazeportLauncher \\ # 定制桌面 Launcher BazeportSystem # 系统工具应用 系统应用通过平台 key 签名可以获得 system 级别的权限，这是厂商定制的常用手段。\n3. 分区架构 这是 embedded Android 特有的概念。固件烧录到设备后，存储被划分为多个分区：\nRoss (S905X5M) 的分区布局 从 BoardConfig.mk 中提取的关键分区信息：\n分区 大小 文件系统 内容 bootloader - raw U-Boot（AVB2 + VAB 签名） boot 64MB (67108864) raw kernel + ramdisk dtb 252KB (258048) raw 设备树 blob dtbo 2MB (2097152) raw 设备树 overlay vendor_boot 64MB (67108864) raw vendor 内核模块 + DTB super 3.2GB (3355443200) 逻辑卷 包含以下动态分区 ├ system - erofs Android 系统镜像 ├ vendor - erofs 厂商专有文件 ├ product - erofs 产品特定配置 ├ system_ext - erofs 系统扩展 ├ odm 16MB (16777216) ext4 设备 OEM 定制 └ vendor_dlkm - erofs 可动态加载的内核模块 userdata 550MB (576716800) f2fs 用户数据（应用、设置） metadata - - 加密 metadata 注: Android 10+ 使用动态分区（super 分区），system/vendor/product 不再是物理分区，而是 super 分区内的逻辑卷。这可以灵活调整各分区大小。\n分区挂载流程 Boot ROM ↓ 读取启动设备 Bootloader (U-Boot) ↓ 验证 vbmeta → 验证 boot.img ↓ 加载 kernel + ramdisk 到内存 Kernel ↓ init 进程启动 ↓ 解析 fstab 挂载分区 ├── mount /system (erofs, 只读) ├── mount /vendor (erofs, 只读) ├── mount /product (erofs, 只读) └── mount /data (f2fs, 读写) 参考代码库中的 fstab:\ndevice/amlogic/ross/fstab.ab_oem.amlogic 4. 启动流程完整时序 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐ │ Boot ROM │───→│ U-Boot │───→│ Kernel │───→│ init │───→| Zygote │ │ (芯片固件) │ │ (启动加载器) │ │ (Linux) │ │ (PID 1) │ │ (Java VM)│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ └─────────┘ │ │ ↓ ↓ init.rc System Server │ │ ↓ ↓ Vendor Init AMS/PMS/WMS... (init.amlogic. │ board.rc) ↓ Launcher (桌面启动) 关键说明:\nBoot ROM: 芯片出厂固化的代码，上电后从 eMMC/NAND 加载 bootloader U-Boot: 初始化硬件(ddr、时钟)、验证启动镜像签名(AVB2)、加载 kernel Kernel: 启动内核、初始化驱动、挂载根文件系统 init: PID 1，执行 init.rc 脚本启动核心服务 Zygote: Android Java 虚拟机进程，所有 Java 应用的父进程 System Server: 启动所有 Java 系统服务 Launcher: 桌面应用启动，用户看到 UI 5. 代码库目录结构对照 目录 对应架构层 说明 bootloader/uboot-repo/ Bootloader U-Boot 源码 kernel/ Kernel Linux 内核 hardware/amlogic/ HAL Amlogic 硬件抽象层（gralloc, hwcomposer, camera 等） hardware/interfaces/ HAL HIDL 接口定义 device/amlogic/ HAL + 设备配置 设备树、BoardConfig、init rc vendor/giec/ 各层 厂商全栈定制（HAL 服务、SELinux、APK） frameworks/base/ Framework Java Framework、系统服务 frameworks/native/ Framework + Native Native 服务、SurfaceFlinger packages/ 应用 系统应用 system/core/ 核心工具 init、adbd、logd build/make/soong/ 构建系统 Android.bp / Makefile 构建规则 6. 自问自答（检验理解） Q: Android 和传统 Linux embedded 的主要区别是什么？ A: 主要是框架层。底层还是 Linux 内核，但上层套了 Java Framework、HAL 隔离层、Binder IPC、SELinux、独特的构建系统。对嵌入式开发者来说，从 kernel 一路往上看到 HAL 层是平滑过渡的。\nQ: Project Treble 解决了什么问题？ A: 以前厂商改 HAL 必须连 Framework 一起更新，导致 Android 版本升级困难。Treble 用 HIDL 把 HAL 标准化、独立化，Framework 和厂商 HAL 可以分别更新。\nQ: super 分区和传统分区有什么不同？ A: 以前 system、vendor 都是独立的物理分区，大小固定不能改。Android 10+ 用 super 动态分区，把这些分区变成 super 内的逻辑卷，空间利用率更高，OTA 更新也更灵活。\nQ: boot.img 里有什么？ A: 包含 kernel（Linux 内核）+ ramdisk（init 程序和挂载脚本）。Android 13+ 还引入了 init_boot.img 把 generic ramdisk 独立出来，实现 GKI（通用内核镜像）。\n7. 下一步 理解架构后，下一步建议进入 Phase 1.2: 构建系统入门，了解 Android 如何用 Android.bp/Android.mk 组织代码编译。\n如果对某层特别感兴趣，也可以深入：\nLauncher/APK 层 → 看 vendor/giec/apps/BazeportLauncher/ HAL 层 → 看 hardware/amlogic/hwcomposer/ 或 vendor/giec/hardware/interfaces/ SELinux → 看 vendor/giec/common/sepolicy/ init 启动 → 看 device/amlogic/ross/init.amlogic.board.rc ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/01-android-%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88/","summary":"\u003ch1 id=\"android-系统架构总览\"\u003eAndroid 系统架构总览\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e学习日期: 2026-04-28\n参考项目: Amlogic S905X5M (ross) Android 14 TV\n前置知识: Linux embedded C/C++ 基础\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-android-分层架构\"\u003e1. Android 分层架构\u003c/h2\u003e\n\u003cp\u003eAndroid 系统从上到下分为 5 层，每一层都依赖下一层提供的服务：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e ┌─────────────────────────────────────────────────┐\n │                  Applications                   │  ← 系统应用 + 第三方应用\n │   Launcher, Settings, Browser, OTA Client...    │\n ├─────────────────────────────────────────────────┤\n │              Application Framework              │  ← 应用框架（Java）\n │   ActivityManager, WindowManager, PackageManager│\n ├─────────────────────────────────────────────────┤\n │              Android System Services            │  ← 系统服务（Native + Java）\n │   System Server, SurfaceFlinger, MediaServer... │\n ├─────────────────────────────────────────────────┤\n │         Hardware Abstraction Layer (HAL)        │  ← 硬件抽象层（C/C++）\n │   Camera HAL, Audio HAL, Graphics HAL...        │\n ├─────────────────────────────────────────────────┤\n │              Linux Kernel                       │  ← Linux 内核 + 驱动\n │   Display Driver, WiFi, BT, Audio, DMA-BUF...   │\n └─────────────────────────────────────────────────┘\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"与传统-linux-embedded-的对比\"\u003e与传统 Linux embedded 的对比\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e概念\u003c/th\u003e\n          \u003cth\u003e传统 Linux\u003c/th\u003e\n          \u003cth\u003eAndroid\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e内核\u003c/td\u003e\n          \u003ctd\u003eLinux kernel\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e同一内核\u003c/strong\u003e + Android 特有驱动 (ashmem/binder/ion)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003einit 系统\u003c/td\u003e\n          \u003ctd\u003esystemd/busybox init\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003einit\u003c/strong\u003e（专属，解析 .rc 文件）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eIPC\u003c/td\u003e\n          \u003ctd\u003eD-Bus/socket\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eBinder\u003c/strong\u003e（Android 核心 IPC 机制）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e设备访问\u003c/td\u003e\n          \u003ctd\u003e直接 open /dev/xxx\u003c/td\u003e\n          \u003ctd\u003e通过 \u003cstrong\u003eHAL\u003c/strong\u003e + HIDL 接口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e应用格式\u003c/td\u003e\n          \u003ctd\u003eELF 可执行文件\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eAPK\u003c/strong\u003e（Java + Native libs）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e权限控制\u003c/td\u003e\n          \u003ctd\u003eDAC (user/group)\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eDAC + SELinux\u003c/strong\u003e（强制）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e构建系统\u003c/td\u003e\n          \u003ctd\u003eMakefile/CMake\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eSoong (Android.bp)\u003c/strong\u003e + Makefile\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-各层详解结合参考代码库\"\u003e2. 各层详解（结合参考代码库）\u003c/h2\u003e\n\u003ch3 id=\"21-layer-1-linux-kernel\"\u003e2.1 Layer 1: Linux Kernel\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e位置\u003c/strong\u003e: kernel 源码 + 设备树 DTS\u003c/p\u003e","title":"01-Android系统架构总览"},{"content":"Android 嵌入式全栈学习计划 背景: 有 Linux embedded C/C++ 基础（浅层），接触过 BSP API 适配 目标: 掌握 Android 嵌入式全栈开发，能独立完成从底层到应用层的系统定制 参考代码库: ~/android/aml/s905x5/aml-s905x5-androidu-v2/（Android 14 + Amlogic S905X5M ATV）\n总览 Phase 1 ─ 筑基 ──────────────────────────────── 2-3 周 构建系统入门 + AOSP 架构 + 基本 Java Phase 2 ─ 厂商定制层 ──────────────────────────── 3-4 周 从 vendor/giec/ 读懂\u0026#34;厂商如何改系统\u0026#34; Phase 3 ─ 深入底层 ───────────────────────────── 4-6 周 Bootloader → Kernel → HAL → SELinux Phase 4 ─ Framework 与应用 ────────────────────── 4-6 周 系统服务 → 应用开发 → OTA Phase 1：筑基（预计 2-3 周） 1.1 理解 Android 系统架构 目标：搞懂 Android 系统的分层结构，建立整体地图。\nTODO:\n阅读 Android 架构图 官方文档 理清分层：App → Framework → 系统服务 → HAL → Kernel 理解分区：bootloader / boot / system / vendor / product / data / cache 输出笔记: notes/01-android-架构概览.md\n1.2 Android 构建系统入门 目标：能读懂 Android.bp 和 Android.mk，知道一个模块如何被编译进系统。\nTODO:\n学习 Soong (Android.bp) 基本语法：cc_binary、cc_library、android_app、cc_defaults 学习 Android.mk 基本语法：include $(CLEAR_VARS) / LOCAL_MODULE / LOCAL_SRC_FILES / include $(BUILD_...) 理解 PRODUCT_PACKAGES 如何把模块打包进镜像 理解 $(call inherit-product, ...) 的包含链 结合参考代码库：打开 build/make/core/main.mk 入口，宏观理解构建流程 输出笔记: notes/02-android-构建系统.md 实操: 在参考代码库中找 1 个 cc_binary 和 1 个 android_app 的 Android.bp，读通它。\n1.3 Java 基础（够用就行） 目标：能读懂 Android Framework 和 APK 源码，不要求能写复杂应用。\nTODO:\nJava 基本语法：类、继承、接口、抽象类 理解 package / import 机制 异常处理：try-catch、throws Android 特有的 Java 用法：Context、Intent、Activity、Service、BroadcastReceiver 不建议: 啃完整 Java 教程。遇到代码时查，配合 IDE 提示学习最快。\n输出笔记: notes/03-java-速查表.md（自己整理的关键语法速查）\nPhase 2：厂商定制层（预计 3-4 周） 这是参考代码库最有价值的部分。绕过 AOSP 大海量的通用代码，直接看厂商写了什么。\n2.1 从 device-giec.mk 出发 目标：把 device-giec.mk 中每一行提到的文件都找到并理解其作用。\nTODO:\n读通 vendor/giec/device-giec.mk（已完成） 逐行追踪包含的每个文件和模块： vendor/giec/common/sepolicy/ — SELinux 策略 vendor/giec/common/manifest.xml — HIDL 服务声明 vendor/giec/common/compatibility_matrix.xml — Framework 需求声明 vendor/giec/common/hwstbcmdservice/ — HIDL 服务实现 vendor/giec/java-libs/shellCmd.jar — 系统应用特权库 输出笔记: notes/04-厂商定制层.md\n2.2 预置 APK 分析 目标：理解厂商预置应用的代码结构和构建方式。\nTODO:\n阅读 vendor/giec/apps/OTAClient/ — OTA 升级客户端（涉及网络请求、系统更新流程） 阅读 vendor/giec/apps/BazeportLauncher/ — 定制 Launcher 了解 APK 签名机制：testkey vs release key 输出笔记: notes/05-预置APK分析.md\n2.3 设备配置 目标：理解一个 device 的完整配置。\nTODO:\n选择 device/amlogic/ross/（S905X5M）作为分析对象 读通它的 device.mk / BoardConfig.mk / AndroidBoard.mk 理解它如何选择 kernel、uboot、分区大小、硬件特性 输出笔记: notes/06-设备配置.md\nPhase 3：深入底层（预计 4-6 周） 这是你 C/C++ 背景能发挥的阶段。\n3.1 Bootloader：U-Boot + 安全启动 TODO:\n阅读 bootloader/uboot-repo 构建命令：./mk s6_bl201 --vab --avb2 理解 VAB (Verified Android Boot) + AVB2 (Android Verified Boot 2.0) 理解 U-Boot 如何加载 boot.img 并校验签名 了解 fastboot 模式 输出笔记: notes/07-bootloader-uboot-安全启动.md\n3.2 Kernel 构建与设备树 TODO:\n理解 ./mk ross -v common14-5.15 — Kernel 版本选择与构建 找到设备树文件：device/amlogic/ross/ 下的 DTS/DTSI 对比 Linux 主线 DTS 和 Android 设备树的差异 输出笔记: notes/08-kernel-设备树.md\n3.3 HAL 与 Treble TODO:\n理解 HIDL：接口定义语言和代码生成 分析 vendor/giec/hardware/interfaces/ 的 HIDL 定义 理解 hwservicemanager、服务注册与客户端获取服务的完整流程 对比：传统 Linux 驱动 ioctl 方式 vs Android HIDL 方式 输出笔记: notes/09-hal-treble-hidl接口.md\n3.4 SELinux TODO:\n读通 vendor/giec/common/sepolicy/ 下的所有 .te 文件 理解 type、allow、domain、context 等核心概念 理解 Android 独有的 sepolicy 语法（file_type、domain、typeattribute） 实操：尝试加一条 SELinux 规则并编译验证 输出笔记: notes/10-selinux-安全策略.md\nPhase 4：Framework 与应用（预计 4-6 周） 4.1 Android Framework 核心服务 TODO:\n理解 System Server 启动流程（frameworks/base/services/java/com/android/server/SystemServer.java） 了解核心服务：ActivityManagerService、PackageManagerService、WindowManagerService 理解 Binder IPC 机制（对标 Linux D-Bus） 输出笔记: notes/11-framework-核心服务.md\n4.2 Android TV 特性 TODO:\n了解 TV Input Framework (TIF) 了解 TV 特有的 UI/UX 设计（Leanback UI） 理解参考代码库中如何集成 MindTheGapps 输出笔记: notes/12-android-tv-特性.md\n4.3 OTA 升级系统 TODO:\n分析 OTA 升级包的生成流程（./build.sh -o） 分析 vendor/giec/apps/OTAClient/ 的升级逻辑 理解 Recovery 模式（bootable/recovery/） 输出笔记: notes/13-ota-升级系统.md\n4.4 系统应用开发入门 TODO:\n用 Java 写一个简单的系统应用（需签名平台 key） 通过 shellCmd.jar 调用系统级命令 编译进系统并刷机验证 输出笔记: notes/14-系统应用开发.md\n学习方式建议 读代码原则 先粗后细：先看 Makefile / 目录结构，再深入具体源码 追踪调用链：看到一个函数调用，如 some_func()，按住 Ctrl 点进去看实现 修改验证：光读不强，改一行代码 → 编译 → 刷机验证，印象最深 笔记策略 每个模块输出一篇自己的理解笔记，用大白话写（这是检验真懂的标志） 画图：架构图、调用时序图、分区布局图 记录踩坑：编译错误、SELinux denials、分区空间不足 遇到问题时的排查顺序 编译错误 → 看构建系统的错误输出，搜相同问题 运行时错 → logcat 看日志 权限拒绝 → dmesg | grep avc 看 SELinux 报错 功能异常 → 逐层排查：App → Service → HAL → Kernel 最终成果预期 完成此计划后，你应该能：\n✅ 独立完成一个 Android 设备的 BSP 适配（kernel + uboot + device 配置） ✅ 添加新的硬件 HIDL 服务并编写 sepolicy 规则 ✅ 预置/开发系统应用，理解签名和权限机制 ✅ 执行完整构建并生成 OTA 包 ✅ 看懂 AOSP 大部分代码结构，能定位问题所在层 提示: 这是一个参考计划，每个阶段的时长可根据自己的节奏调整。关键是每一步都要有笔记输出，写下来才是自己的。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/00-learning-plan/","summary":"\u003ch1 id=\"android-嵌入式全栈学习计划\"\u003eAndroid 嵌入式全栈学习计划\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e背景\u003c/strong\u003e: 有 Linux embedded C/C++ 基础（浅层），接触过 BSP API 适配\n\u003cstrong\u003e目标\u003c/strong\u003e: 掌握 Android 嵌入式全栈开发，能独立完成从底层到应用层的系统定制\n\u003cstrong\u003e参考代码库\u003c/strong\u003e: \u003ccode\u003e~/android/aml/s905x5/aml-s905x5-androidu-v2/\u003c/code\u003e（Android 14 + Amlogic S905X5M ATV）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"总览\"\u003e总览\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ePhase 1 ─ 筑基 ──────────────────────────────── 2-3 周\n  构建系统入门 + AOSP 架构 + 基本 Java\n\nPhase 2 ─ 厂商定制层 ──────────────────────────── 3-4 周\n  从 vendor/giec/ 读懂\u0026#34;厂商如何改系统\u0026#34;\n\nPhase 3 ─ 深入底层 ───────────────────────────── 4-6 周\n  Bootloader → Kernel → HAL → SELinux\n\nPhase 4 ─ Framework 与应用 ────────────────────── 4-6 周\n  系统服务 → 应用开发 → OTA\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"phase-1筑基预计-2-3-周\"\u003ePhase 1：筑基（预计 2-3 周）\u003c/h2\u003e\n\u003ch3 id=\"11-理解-android-系统架构\"\u003e1.1 理解 Android 系统架构\u003c/h3\u003e\n\u003cp\u003e目标：搞懂 Android 系统的分层结构，建立整体地图。\u003c/p\u003e","title":"00-Android嵌入式全栈学习计划"},{"content":"Android Framework Console 命令速查 适用场景：adb shell 下执行的 Android framework 层调试命令 覆盖工具：am / pm / dumpsys / settings / wm / input / service / cmd / appops / content / ime / monkey / svc / media\n目录 am — Activity Manager pm — Package Manager dumpsys — 系统服务诊断 settings — 系统设置 wm — Window Manager input — 输入事件模拟 service — Service 调用 cmd — 服务命令接口 appops — 应用权限管理 content — ContentProvider 操作 ime — 输入法管理 monkey — UI 压力测试 svc — 网络/电源服务控制 media — 多媒体控制 其他实用命令 1. am — Activity Manager 管理 Activity、Service、Broadcast、Intent 的启动与调试。\n1.1 启动 Activity # 标准启动（指定完整 Action） am start -a android.intent.action.VIEW -d http://www.google.com # 启动指定包名/Activity am start -n com.android.settings/.Settings am start -n com.android.camera/.Camera # 带 extra 参数 am start -n com.android.settings/.Settings \\ -e :android:show_fragment wifi # 启动并传递 String 类型的 extra am start -n com.example/.MainActivity \\ -e key1 value1 -e key2 value2 # 启动并传递 int/boolean/float 类型的 extra am start -n com.example/.MainActivity \\ --ei int_key 123 \\ --ez bool_key true \\ --ef float_key 1.5 # 以 FLAG_ACTIVITY_NEW_TASK 方式启动 am start -n com.example/.MainActivity -f 0x10000000 # 指定 action 和 category am start -a android.intent.action.MAIN \\ -c android.intent.category.HOME # 启动 Activity 并等待结果（用于测试） am start -W -n com.android.settings/.Settings # 输出：WaitTime / CallTime / TotalTime 常用参数：\n参数 说明 -a \u0026lt;ACTION\u0026gt; Intent Action -d \u0026lt;DATA_URI\u0026gt; Intent Data URI -n \u0026lt;COMPONENT\u0026gt; 完整组件名（package/activity） -t \u0026lt;MIME_TYPE\u0026gt; MIME 类型 -e \u0026lt;KEY\u0026gt; \u0026lt;VALUE\u0026gt; String Extra --ei \u0026lt;KEY\u0026gt; \u0026lt;VALUE\u0026gt; int Extra --ez \u0026lt;KEY\u0026gt; \u0026lt;VALUE\u0026gt; boolean Extra --ef \u0026lt;KEY\u0026gt; \u0026lt;VALUE\u0026gt; float Extra -f \u0026lt;FLAGS\u0026gt; Intent Flags（十六进制） -c \u0026lt;CATEGORY\u0026gt; Intent Category -W 等待启动完成并打印耗时 1.2 启动 Service # 启动 Service am startservice -n com.example/.MyService # 带 extra 启动 Service am startservice -n com.example/.MyService -e cmd play # 绑定 Service 并发送命令 am startservice -a com.example.action.START \\ --es cmd \u0026#34;start_playback\u0026#34; # Android 12+ 使用 foreground service 需声明 am start-foreground-service -n com.example/.MyService 1.3 发送广播 # 发送显式广播 am broadcast -n com.example/.MyReceiver # 发送标准广播（如开机完成） am broadcast -a android.intent.action.BOOT_COMPLETED # 发送带权限的广播 am broadcast -a com.example.CUSTOM_BROADCAST \\ --es msg \u0026#34;hello\u0026#34; \\ --receiver-permission com.example.PERMISSION # 发送有序广播 am broadcast -a com.example.ORDERED_BROADCAST --ordered 常用标准 Action：\nAction 说明 android.intent.action.BOOT_COMPLETED 开机完成 android.intent.action.SCREEN_OFF 屏幕关闭 android.intent.action.SCREEN_ON 屏幕点亮 android.intent.action.BATTERY_LOW 电量低 android.intent.action.BATTERY_OKAY 电量恢复 android.intent.action.TIME_SET 时间设置 android.intent.action.ACTION_POWER_CONNECTED 连接电源 android.intent.action.ACTION_POWER_DISCONNECTED 断开电源 1.4 进程管理 # 查看当前 Activity 栈 am stack list # 查看所有可见 Activity am monitor # 每次有 Activity 启动时打印信息 # 强制停止应用 am force-stop com.example.app # 杀死进程（不推荐，用 force-stop） am kill com.example.app # 杀死所有后台进程 am kill-all # 设置应用为不活动（idle） am idle # 获取当前焦点 Activity # 方式一： dumpsys activity | grep mResumedActivity # 方式二： dumpsys window | grep mCurrentFocus # 设置屏幕冻结/解冻状态 am screen-compat on/off \u0026lt;package\u0026gt; 2. pm — Package Manager 管理应用安装、卸载、权限、组件状态。\n2.1 列出应用 # 列出所有已安装应用 pm list packages # 仅列出第三方应用 pm list packages -3 # 仅列出系统应用 pm list packages -s # 按关键字搜索 pm list packages | grep giec pm list packages | grep google # 显示 APK 路径 pm list packages -f pm list packages -f | grep camera # 显示安装来源 pm list packages -i # 显示原生库路径 pm list packages -l # 显示应用特征（是否为系统/第三方/已禁用等） pm list packages --show-stored-sizes # 显示已卸载但仍保留数据的应用 pm list packages -u # 显示已禁用的应用 pm list packages -d # 显示已启用的应用 pm list packages -e # 仅显示有调试模式的应用 pm list packages --debug # 显示应用的 ABI pm list packages --abi 常用过滤器：\npm list packages [options] [filter] # 按包名前缀过滤 pm list packages android. pm list packages com.android. pm list packages com.google. # 按 UID 过滤 pm list packages --uid 1000 # 显示包名包含特定字符串 pm list packages -f | grep launcher 2.2 安装与卸载 # 安装 APK pm install /data/local/tmp/app.apk pm install -r /data/local/tmp/app.apk # 覆盖安装（保留数据） pm install -d /data/local/tmp/app.apk # 降级安装（版本号更低时用） pm install -g /data/local/tmp/app.apk # 授予所有运行时权限 pm install -t /data/local/tmp/app.apk # 允许安装测试 APK pm install -i com.android.vending /data/local/tmp/app.apk # 指定安装来源 # 多用户安装 pm install --user 10 /data/local/tmp/app.apk # 卸载应用 pm uninstall com.example.app pm uninstall -k com.example.app # 卸载但保留数据/cache pm uninstall --user 10 com.example.app # 卸载指定用户的 # 清除应用数据 pm clear com.example.app pm install 常见失败原因：\n错误 原因 INSTALL_FAILED_ALREADY_EXISTS 已安装，需加 -r INSTALL_FAILED_UPDATE_INCOMPATIBLE 签名不一致，需完全卸载 INSTALL_FAILED_VERSION_DOWNGRADE 降级安装，需加 -d INSTALL_FAILED_INVALID_APK APK 损坏或不完整 INSTALL_FAILED_INSUFFICIENT_STORAGE 存储空间不足 INSTALL_FAILED_USER_RESTRICTED 用户限制（如多用户配置） INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE targetSdk 降级导致权限模型变化 2.3 查询应用信息 # 查看应用详细信息 pm dump com.example.app pm dump com.example.app | grep -A 20 \u0026#34;permissions\u0026#34; pm dump com.example.app | grep -A 10 \u0026#34;activities\u0026#34; # 查看应用路径 pm path com.example.app # 输出：package:/data/app/com.example.app-xxx/base.apk # 查看应用版本 pm dump com.example.app | grep versionName # 查看应用 targetSdk pm dump com.example.app | grep targetSdk # 查看应用 UID pm list packages -U | grep com.example.app # 查看应用的启动 Activity pm resolve-activity --brief com.example.app pm dump com.example.app | grep -A 3 \u0026#34;MainActivity\u0026#34; # 查看应用使用的库 pm dump com.example.app | grep \u0026#34;shared libraries\u0026#34; 2.4 组件管理（启用/禁用） # 禁用应用（用户不可见，无法启动） pm disable com.example.app # 启用应用 pm enable com.example.app # 禁用 Activity pm disable com.example.app/.SomeActivity # 启用 Activity pm enable com.example.app/.SomeActivity # 隐藏应用（Launcher 中不可见，但仍可启动） pm hide com.example.app # 取消隐藏 pm unhide com.example.app # 挂起应用（冻结应用进程） pm suspend com.example.app # 取消挂起 pm unsuspend com.example.app # 设置应用为默认启动器 pm set-home-activity com.example.app/.MainActivity 2.5 权限管理 # 列出应用声明的权限 pm list permissions -d -g com.example.app # 列出应用已授予的运行时权限 pm dump com.example.app | grep -A 200 \u0026#34;runtime permissions\u0026#34; | head -50 # 授予权限 pm grant com.example.app android.permission.CAMERA pm grant com.example.app android.permission.ACCESS_FINE_LOCATION # 撤销权限 pm revoke com.example.app android.permission.CAMERA # 重置所有已授予的运行权限 pm reset-permissions com.example.app # 列出所有 dangerous 权限 pm list permissions -d -g # 列出所有分组 pm list permission-groups # 根据权限分组列出 pm list permissions -g # 检查权限是否已授予 pm check-permission android.permission.CAMERA com.example.app 2.6 多用户 # 创建新用户 pm create-user \u0026lt;user_name\u0026gt; # 删除用户 pm remove-user \u0026lt;user_id\u0026gt; # 列出所有用户 pm list users # 为指定用户安装/卸载 pm install --user 10 /path/to/app.apk pm uninstall --user 10 com.example.app # 获取用户 ID pm get-primary-storage-user-id 3. dumpsys — 系统服务诊断 dumpsys 是 Android 最重要的诊断工具，用于查询系统服务的内部状态。\n3.1 基础用法 # 列出所有可用服务 dumpsys -l # 查询指定服务 dumpsys \u0026lt;service_name\u0026gt; dumpsys activity dumpsys window dumpsys package # 服务帮助信息（查看支持哪些参数） dumpsys \u0026lt;service_name\u0026gt; -h dumpsys activity -h dumpsys package -h # 输出到文件 dumpsys activity \u0026gt; /data/local/tmp/activity.txt 3.2 Activity 相关 # 当前焦点 Activity dumpsys activity | grep -E \u0026#34;mResumedActivity|mFocusedActivity\u0026#34; # 当前 Activity 栈 dumpsys activity activities # 最近 Activity 历史 dumpsys activity recents # 所有正在运行的进程 dumpsys activity processes # 查看进程中的 Service dumpsys activity services # 查看 BroadcastReceiver 状态 dumpsys activity broadcasts # 查看 Provider 状态 dumpsys activity providers # 查看 oom_adj（重要：判断进程是否被杀） dumpsys activity processes | grep com.example.app oom_adj 值含义：\n值 含义 -17 系统核心进程（永远不会被杀） 0~100 前台进程 100~200 可见进程 200~300 服务进程 300~900 后台进程（越靠后越先被杀） 900+ 空进程 3.3 Package / 包管理 # 查看包基本信息 dumpsys package com.example.app # 查看包中所有 Activity dumpsys package com.example.app | grep -A 5 \u0026#34;Activity\u0026#34; # 查看包中注册的 Service dumpsys package com.example.app | grep -A 5 \u0026#34;Service\u0026#34; # 查看包中注册的 Receiver dumpsys package com.example.app | grep -A 5 \u0026#34;Receiver\u0026#34; # 查看包权限 dumpsys package com.example.app | grep -A 50 \u0026#34;runtime permissions\u0026#34; # 查看包安装信息 dumpsys package com.example.app | grep -E \u0026#34;version|install|firstInstall|lastUpdate\u0026#34; # 查看包的签名信息 dumpsys package com.example.app | grep -A 10 \u0026#34;Signatures\u0026#34; # 系统中所有包的信息汇总（通常输出很大） dumpsys package packages 3.4 Window 显示相关 # 当前窗口焦点（最常用） dumpsys window | grep mCurrentFocus # 当前窗口栈 dumpsys window windows # 显示信息（分辨率、密度、刷新率） dumpsys window displays # 查看输入法窗口 dumpsys window | grep InputMethod # 查看 Surface 信息 dumpsys window surfaces # 查看动画状态 dumpsys window animations # 查看屏幕旋转状态 dumpsys window | grep rotation 3.5 内存相关 # 系统内存概览 dumpsys meminfo # 查看进程内存详情 dumpsys meminfo \u0026lt;pid\u0026gt; dumpsys meminfo com.example.app # 查看 GPU 内存使用 dumpsys gfxinfo com.example.app # 查看 Graphics 内存 dumpsys graphicsstats # 查看数据库内存 dumpsys dbinfo com.example.app 3.6 其他常用服务 # 电池状态 dumpsys battery dumpsys battery set level 50 # 模拟电量 50% dumpsys battery set status 1 # 模拟电池状态（1=未知, 2=充电, 3=放电, 4=未充, 5=满） dumpsys battery unplug # 模拟断开充电器 dumpsys battery reset # 重置电池模拟 # 电源管理 dumpsys power dumpsys power | grep \u0026#34;Wake Locks\u0026#34; # 查看 wakelock # 网络 dumpsys wifi dumpsys connectivity dumpsys netstats dumpsys ethernet # 蓝牙 dumpsys bluetooth_manager # 显示 dumpsys display # 通知 dumpsys notification # 设置（自定义设置服务） dumpsys settings # 磁盘使用 dumpsys diskstats # CPU 信息 dumpsys cpuinfo # 输入法 dumpsys input_method # 位置 dumpsys location # 媒体 dumpsys media_session dumpsys audio # 传感器 dumpsys sensorservice # 壁纸 dumpsys wallpaper # 用户管理 dumpsys user 3.7 dumpsys 服务速查表 服务名 关键用途 activity Activity 栈、进程状态、oom_adj、Service window 焦点窗口、显示参数、窗口层级 package 包信息、权限、签名、组件注册 meminfo 内存占用（PSS/RSS/USS） power wakelock、屏幕待机状态 battery 电量、温度、充电状态 batterystats 电量历史统计（需先 dumpsys batterystats --reset） wifi Wi-Fi 连接信息 connectivity 网络连接状态 netstats 网络流量统计 bluetooth_manager 蓝牙状态 display 显示设备参数 notification 通知栏内容 media_session 媒体播放/暂停状态 diskstats 磁盘分区使用量 cpuinfo CPU 负载 input_method 输入法状态 gfxinfo GPU 渲染性能 sensorservice 传感器数据 user 多用户状态 4. settings — 系统设置 读写 Settings 命名空间（system/secure/global），修改系统配置。\n4.1 读取设置 # 读取所有设置（三个命名空间） settings list system settings list secure settings list global # 读取指定 key settings get system screen_brightness settings get secure android_id settings get global airplane_mode_on # 读取并查看类型 settings get system screen_brightness # 返回数值 settings get global device_name # 返回字符串 4.2 写入设置 # 写入值 settings put system screen_brightness 200 settings put global airplane_mode_on 1 settings put secure lock_screen_show_notifications 1 # 写入字符串 settings put global device_name \u0026#34;My Android TV\u0026#34; # 写入 float settings put global window_animation_scale 0.5 # 删除 key（恢复默认） settings delete system screen_brightness 4.3 常用设置项 # 窗口动画速度（0=关闭动画） settings put global window_animation_scale 0 settings put global transition_animation_scale 0 settings put global animator_duration_scale 0 # 屏幕超时（毫秒） settings put system screen_off_timeout 300000 # 屏幕亮度 0-255 settings put system screen_brightness 120 # 屏幕自动亮度 settings put system screen_brightness_mode 1 # 开启 settings put system screen_brightness_mode 0 # 关闭 # 字体大小 settings put system font_scale 1.0 # 时区 settings put global timezone Asia/Shanghai # 日期格式 settings put system date_format yyyy-MM-dd # 屏幕旋转 settings put global accelerometer_rotation 1 # 自动旋转 settings put global accelerometer_rotation 0 # 锁定 # 开发者选项 settings put global development_settings_enabled 1 # 显示屏幕刷新率 settings put global show_refresh_rate 1 settings put global show_fps_info 1 # 显示触摸点 settings put global show_touches 1 # 不保留 Activity（退出即销毁） settings put global always_finish_activities 1 # 应用兼容模式（强制跑在 32 位） settings put global development_settings_enabled 1 # 需先开启开发者选项 # 然后在开发者选项中设置 三个命名空间的区别：\n命名空间 权限 多用户 用途 system 可写 每用户独立 用户个人偏好（亮度、音量、字体） secure 需 WRITE_SECURE_SETTINGS（系统级签名） 每用户独立 安全相关（锁屏、定位） global 需 WRITE_SECURE_SETTINGS 全局 系统级配置（飞行模式、时区） 5. wm — Window Manager 查询和修改窗口管理器的参数（分辨率、密度、缩放）。\n# 显示当前参数 wm size # 显示分辨率（如 1920x1080） wm density # 显示密度 dpi（如 320） wm overscan # 显示过扫描区域 # 修改分辨率（用于模拟不同屏幕） wm size 1280x720 wm size 1920x1080 wm size reset # 恢复原始分辨率 # 修改密度 wm density 160 wm density 240 wm density reset # 恢复原始密度 # 设置过扫描（移除边缘像素，解决 overscan 问题） wm overscan 0,0,0,0 # reset wm overscan 10,10,10,10 # 四边各收缩 10px # 查看屏幕截图尺寸 wm size -w # 宽度 wm size -h # 高度 使用场景：\n用 wm size 模拟手机/Pad/电视不同分辨率 用 wm density 测试 UI 在不同 dpi 下的布局 wm overscan 对适配电视的过扫描裁剪很有用 6. input — 输入事件模拟 模拟触摸、按键、滚轮等输入事件。\n6.1 按键事件 # 模拟按键（KEYCODE） input keyevent KEYCODE_HOME input keyevent KEYCODE_BACK input keyevent KEYCODE_MENU input keyevent KEYCODE_DPAD_UP input keyevent KEYCODE_DPAD_DOWN input keyevent KEYCODE_DPAD_LEFT input keyevent KEYCODE_DPAD_RIGHT input keyevent KEYCODE_DPAD_CENTER input keyevent KEYCODE_ENTER input keyevent KEYCODE_DEL input keyevent KEYCODE_VOLUME_UP input keyevent KEYCODE_VOLUME_DOWN input keyevent KEYCODE_MUTE input keyevent KEYCODE_POWER input keyevent KEYCODE_CAMERA input keyevent KEYCODE_APP_SWITCH # 组合键模拟 input keyevent KEYCODE_WAKEUP # 唤醒屏幕 input keyevent KEYCODE_SLEEP # 睡眠 # 游戏/媒体按键 input keyevent KEYCODE_MEDIA_PLAY_PAUSE input keyevent KEYCODE_MEDIA_STOP input keyevent KEYCODE_MEDIA_NEXT input keyevent KEYCODE_MEDIA_PREVIOUS # 电视专用键 input keyevent KEYCODE_DPAD_UP # 方向键上 input keyevent KEYCODE_DPAD_DOWN # 方向键下 input keyevent KEYCODE_DPAD_LEFT # 方向键左 input keyevent KEYCODE_DPAD_RIGHT # 方向键右 input keyevent KEYCODE_DPAD_CENTER # 确认 input keyevent KEYCODE_BACK # 返回 input keyevent KEYCODE_HOME # 主页 6.2 触摸事件 # 点击指定坐标 input tap x y input tap 500 500 input tap 100 200 # 模拟滑动 input swipe x1 y1 x2 y2 input swipe 500 500 500 1000 # 从上往下滑 input swipe 500 1000 500 500 # 从下往上滑 # 带持续时间的滑动（毫秒） input swipe 500 500 500 1000 2000 # 2 秒内从上滑到下 # 拖拽（长按 + 滑动） input draganddrop x1 y1 x2 y2 duration_ms input draganddrop 300 300 300 500 1000 # 长按（TextSelection 需要） input swipe x y x y duration_ms input swipe 500 500 500 500 2000 # 在 (500,500) 处长按 2 秒 6.3 文本输入 # 输入文本（需要输入框有焦点） input text \u0026#34;Hello World\u0026#34; input text \u0026#34;adb shell input test\u0026#34; input text \u0026#34;你好世界\u0026#34; # 输入特殊字符（需转义） input text \u0026#34;password123!\u0026#34; input text \u0026#34;user@example.com\u0026#34; # 清空输入框再输入（模拟全选+删除+输入） input keyevent KEYCODE_MOVE_END input keyevent --longpress KEYCODE_DEL input text \u0026#34;new text\u0026#34; 6.4 滚轮事件 # 模拟滚轮（电视遥控器/鼠标滚轮） input roll dx dy input roll 0 -10 # 向上滚动 input roll 0 10 # 向下滚动 input roll 10 0 # 向右滚动 常用 KEYCODE 速查 KEYCODE 值 说明 HOME 3 Home 键 BACK 4 返回键 MENU 82 菜单键 DPAD_UP 19 方向键上 DPAD_DOWN 20 方向键下 DPAD_LEFT 21 方向键左 DPAD_RIGHT 22 方向键右 DPAD_CENTER 23 确认键 VOLUME_UP 24 音量+ VOLUME_DOWN 25 音量- POWER 26 电源键 ENTER 66 回车键 DEL 67 退格键 MUTE 91 静音 MEDIA_PLAY_PAUSE 85 播放/暂停 MEDIA_STOP 86 停止 APP_SWITCH 187 最近任务 WAKEUP 224 唤醒屏幕 SLEEP 223 睡眠 7. service — Service 调用 调用已注册的 Binder 服务的方法。\n# 列出所有已注册的服务 service list # 查看服务包含的方法 service check \u0026lt;service_name\u0026gt; # 调用服务方法 service call \u0026lt;service_name\u0026gt; \u0026lt;code\u0026gt; [args...] # 获取窗口服务信息（code 1 通常是 list） service call window 1 # 获取 Activity 信息 service call activity 1 # 调用 IPowerManager（设置屏幕亮度） service call power 12 i32 120 # 调用音量服务 service call audio 3 i32 3 i32 7 i32 0 # audio service code 3 = setStreamVolume # i32 3 = STREAM_MUSIC, i32 7 = 音量值, i32 0 = flag 参数类型说明：\n参数 说明 示例 i32 N 32 位整数 i32 123 s16 \u0026quot;str\u0026quot; 16 位字符串 s16 \u0026quot;hello\u0026quot; null 空引用 null f N Float f 1.5 d N Double d 3.14 注意事项：\nservice call 需要知道服务方法的 code 编号，不方便使用 推荐用 cmd 命令（见下节）代替，更直观 service list 显示的服务名可用于 dumpsys 直接查询 8. cmd — 服务命令接口 Android 11+ 引入的服务命令接口，比 service call 更易用。\n# 查看服务支持哪些命令 cmd \u0026lt;service_name\u0026gt; help cmd role help cmd wifi help cmd notification help # 角色管理（Android 10+） cmd role list # 列出所有角色 cmd role add-role-holder \u0026lt;role\u0026gt; \u0026lt;package\u0026gt; # 分配角色 cmd role remove-role-holder \u0026lt;role\u0026gt; \u0026lt;package\u0026gt; # 移除角色 # 角色相关操作示例 cmd role list role:android.role.HOME # 查看当前默认 Launcher cmd role add-role-holder role:android.role.HOME com.example.launcher # 通知管理 cmd notification get_snooze_settings cmd notification set_navigation_bar_color \u0026lt;color\u0026gt; # WiFi 管理 cmd wifi set-wifi-enabled enabled cmd wifi get-wifi-enabled # overlay 管理 cmd overlay list cmd overlay enable \u0026lt;package\u0026gt; cmd overlay disable \u0026lt;package\u0026gt; # sensor 管理 cmd sensorservice set-uid-state \u0026lt;uid\u0026gt; \u0026lt;state\u0026gt; # alarm 管理 cmd alarm list 9. appops — 应用权限管理 AppOps 是 Android 4.4+ 引入的细粒度权限操作管理框架，控制应用对敏感操作的访问。\n# 查看应用的 AppOps 模式 appops get \u0026lt;package\u0026gt; appops get com.example.app # 列出所有 Op appops list # 设置某个 Op 的模式 # mode: allow / deny / ignore / foreground appops set \u0026lt;package\u0026gt; \u0026lt;op\u0026gt; \u0026lt;mode\u0026gt; appops set com.example.app CAMERA deny appops set com.example.app LOCATION allow appops set com.example.app POST_NOTIFICATIONS ignore # 重置所有 AppOps appops reset \u0026lt;package\u0026gt; appops reset com.example.app # 查询特定 op appops query-op \u0026lt;op\u0026gt; \u0026lt;mode\u0026gt; appops query-op CAMERA deny appops query-op LOCATION allow 常用 AppOps：\nOp 说明 CAMERA 相机访问 LOCATION 位置访问 RECORD_AUDIO 录音 READ_CONTACTS 读取联系人 POST_NOTIFICATIONS 发送通知（Android 13+） ACCESS_NOTIFICATIONS 访问通知 SYSTEM_ALERT_WINDOW 悬浮窗 WRITE_SETTINGS 修改系统设置 REQUEST_INSTALL_PACKAGES 安装 APK 10. content — ContentProvider 操作 直接查询、插入、更新、删除 ContentProvider 的数据。\n# 查询（类似 SQL SELECT） content query --uri content://settings/secure content query --uri content://settings/secure \\ --projection name:value content query --uri content://settings/system \\ --where \u0026#34;name=\u0026#39;screen_brightness\u0026#39;\u0026#34; # 插入数据 content insert --uri content://settings/system \\ --bind name:s:screen_brightness \\ --bind value:i:128 # 更新数据 content update --uri content://settings/system \\ --bind value:i:200 \\ --where \u0026#34;name=\u0026#39;screen_brightness\u0026#39;\u0026#34; # 删除数据 content delete --uri content://settings/system \\ --where \u0026#34;name=\u0026#39;screen_brightness\u0026#39;\u0026#34; # 读取联系人 content query --uri content://contacts/people/ # 读取短信 content query --uri content://sms/inbox # 读取日历 content query --uri content://calendar/calendars 参数说明：\n参数 说明 --uri \u0026lt;URI\u0026gt; ContentProvider URI --projection col:col 指定返回列 --where \u0026quot;col='val'\u0026quot; 过滤条件 --bind col:type:val 绑定参数 --sort \u0026quot;col ASC/DESC\u0026quot; 排序 11. ime — 输入法管理 管理输入法（软键盘）的列表、切换和调试。\n# 列出已安装的输入法 ime list ime list -s # 仅显示系统输入法 ime list -a # 显示所有输入法（含第三方） # 启用/禁用输入法 ime enable com.android.inputmethod.latin/.LatinIME ime disable com.example.ime/.MyIME # 设置默认输入法 ime set com.android.inputmethod.latin/.LatinIME # 重置为默认输入法 ime reset # 查看当前输入法 settings get secure default_input_method 12. monkey — UI 压力测试 生成伪随机用户事件流进行压力测试。\n# 基本用法（向系统发送 500 个随机事件） monkey 500 # 指定包名（只向指定应用发事件） monkey -p com.example.app 500 # 指定多个包 monkey -p com.example.app -p com.android.settings 500 # 设置事件间隔（毫秒） monkey -p com.example.app --throttle 200 500 # 调整事件类型比例（百分比） monkey -p com.example.app \\ --pct-touch 50 \\ --pct-motion 20 \\ --pct-trackball 0 \\ --pct-nav 5 \\ --pct-majornav 5 \\ --pct-syskeys 0 \\ --pct-appswitch 10 \\ --pct-anyevent 10 \\ 1000 # 记录日志 monkey -p com.example.app -v -v -v 1000 2\u0026gt;\u0026amp;1 | tee /data/local/tmp/monkey.log # 跟踪 ANR 和崩溃 monkey -p com.example.app \\ --kill-after-error false \\ --ignore-crashes \\ --ignore-timeouts \\ --ignore-security-exceptions \\ --monitor-native-crashes \\ 10000 # 指定种子值（使事件序列可重现） monkey -p com.example.app -s 12345 500 事件类型 --pct-* 说明：\n参数 默认值 说明 --pct-touch 30% 触摸事件（down/up） --pct-motion 15% 滑动事件（down/move/up） --pct-trackball 10% 轨迹球事件 --pct-nav 25% D-pad 导航 --pct-majornav 15% 主要导航（back、menu） --pct-syskeys 2% 系统按键（Home、音量） --pct-appswitch 2% Activity 切换 --pct-anyevent 1% 其他事件 常用选项：\n选项 说明 -v 日志详细级别（-v / -v -v / -v -v -v） -s \u0026lt;seed\u0026gt; 随机数种子（使用相同种子复现相同事件序列） --throttle \u0026lt;ms\u0026gt; 事件间延迟 --ignore-crashes 忽略应用崩溃继续测试 --ignore-timeouts 忽略 ANR 继续测试 --kill-after-error 错误后是否杀死应用 --monitor-native-crashes 监控 native crash 13. svc — 网络/电源服务控制 控制 WiFi、移动数据、蓝牙和电源状态。\n# WiFi 控制 svc wifi enable # 打开 WiFi svc wifi disable # 关闭 WiFi # 移动数据控制 svc data enable # 打开移动数据 svc data disable # 关闭移动数据 # 存储挂载 svc mount external # 挂载外部存储 svc unmount external # 卸载外部存储 # USB 控制 svc usb setState disconnect # 断开 USB svc usb setState connect # 连接 USB # Stay-on（插入电源时不休眠） svc power stayon true # 开启 svc power stayon false # 关闭 svc power stayon usb # USB 供电时保持唤醒 svc power stayon ac # 交流电供电时保持唤醒 svc power stayon wireless # 无线充电时保持唤醒 14. media — 多媒体控制 控制媒体播放服务。\n# 查看媒体会话 media list # 向指定媒体会话发送控制命令 media play \u0026lt;session-id\u0026gt; media pause \u0026lt;session-id\u0026gt; media stop \u0026lt;session-id\u0026gt; media next \u0026lt;session-id\u0026gt; media previous \u0026lt;session-id\u0026gt; # 查看媒体会话详情 media output \u0026lt;session-id\u0026gt; # 查看所有音量组 media volume --show media volume --stream 3 --get # 获取音乐音量 media volume --stream 3 --set 10 # 设置音乐音量为 10 media volume --stream 3 --adj raise # 提高音量 media volume --stream 3 --adj lower # 降低音量 # 音频路由 media routing --show 音频流类型（stream）：\n编号 类型 说明 0 STREAM_VOICE_CALL 通话 1 STREAM_SYSTEM 系统声音 2 STREAM_RING 铃声 3 STREAM_MUSIC 音乐/媒体 4 STREAM_ALARM 闹钟 5 STREAM_NOTIFICATION 通知 6 STREAM_BLUETOOTH_SCO 蓝牙 SCO 10 STREAM_ACCESSIBILITY 无障碍 15. 其他实用命令 15.1 系统信息 # 系统属性（最常用调试命令） getprop # 列出所有属性 getprop ro.build.version.sdk # API level getprop ro.build.version.release # Android 版本 getprop ro.product.name # 产品名 getprop ro.product.cpu.abi # CPU ABI getprop ro.serialno # 序列号 getprop ro.boot.vbmeta.device # vbmeta 设备 getprop persist.sys.timezone # 时区 getprop ro.build.fingerprint # 完整 build 指纹 # 设置系统属性 setprop debug.sf.nobootanimation 1 # 跳过开机动画 setprop persist.vendor.xxx.yyy val # 持久化属性 # 查看 SELinux 状态 getenforce # Enforcing / Permissive setenforce 0 # 设为宽容模式 setenforce 1 # 设为强制模式 # 获取设备 ID settings get secure android_id # 获取当前 UTC 时间 date date -u 15.2 进程管理 # 查看进程 ps -A # 所有进程 ps -A | grep system_server # 过滤特定进程 ps -A -o PID,NAME,%CPU,%MEM # 自定义输出格式 ps -T -p \u0026lt;pid\u0026gt; # 查看进程中的线程 # 查看线程 ps -T -p \u0026lt;pid_of_system_server\u0026gt; # top（实时监控） top -d 1 -n 5 # 每秒刷新，共 5 次 top -d 1 -m 10 # 每秒刷新，显示前 10 行 # 杀死进程 kill \u0026lt;pid\u0026gt; kill -9 \u0026lt;pid\u0026gt; # 强制杀死 # 调试跟踪 strace -p \u0026lt;pid\u0026gt; # 跟踪系统调用 strace -f -p \u0026lt;pid\u0026gt; # 跟踪子进程 strace -e openat -p \u0026lt;pid\u0026gt; # 只跟踪文件打开 # 查看进程打开的文件 ls -l /proc/\u0026lt;pid\u0026gt;/fd/ 15.3 文件系统操作 # 查看磁盘空间 df -h df /data # 查看目录大小 du -sh /data/app/* du -sh /data/data/com.example.app # 统计文件/目录数量 ls -la /data/data/ | wc -l # 查看文件类型 file /system/bin/app_process file /data/app/com.example.app/base.apk # 计算文件校验 md5sum /data/local/tmp/app.apk sha1sum /data/local/tmp/app.apk # 查看系统链接库 ls -l /system/lib64/ ldd /system/bin/app_process64 # 查看动态链接 15.4 锁定与重置 # 锁屏（模拟锁屏状态） input keyevent KEYCODE_SLEEP # 解锁（如果无密码） input keyevent KEYCODE_WAKEUP input swipe 300 1000 300 300 # 滑动解锁 # 重置屏幕超时 settings put system screen_off_timeout 60000 # 删除锁屏密码 # 删除 /data/system 下密码相关文件 rm /data/system/gesture.key rm /data/system/locksettings.db* reboot 15.5 DEBUG 相关 # 生成 bugreport（包含所有诊断信息的压缩包） bugreport # 输出路径：/data/user_de/0/com.android.shell/files/bugreports/ # 或直接： bugreportz \u0026gt; /sdcard/bugreport.zip # 启用/禁用 adb 调试 setprop persist.service.adb.enable 1 setprop persist.service.adb.enable 0 # 跟踪 Binder 调用 # 需要开启 Binder 跟踪 echo 1 \u0026gt; /sys/kernel/debug/tracing/events/binder/binder_transaction/enable cat /sys/kernel/debug/tracing/trace_pipe 15.6 命令组合实战 # 一键获取应用的完整调试信息 PACKAGE=com.example.app echo \u0026#34;===== 路径 =====\u0026#34; pm path $PACKAGE echo \u0026#34;===== 版本 =====\u0026#34; pm dump $PACKAGE | grep version echo \u0026#34;===== 权限 =====\u0026#34; pm dump $PACKAGE | grep -A 10 \u0026#34;runtime permissions\u0026#34; echo \u0026#34;===== Activity 栈 =====\u0026#34; dumpsys activity | grep -E \u0026#34;mResumedActivity|mFocusedActivity\u0026#34; echo \u0026#34;===== 内存 =====\u0026#34; dumpsys meminfo $PACKAGE | head -20 echo \u0026#34;===== oom_adj =====\u0026#34; dumpsys activity processes | grep $PACKAGE echo \u0026#34;===== SELinux 拒绝 =====\u0026#34; dmesg | grep \u0026#34;avc: denied\u0026#34; | grep $PACKAGE | tail -10 echo \u0026#34;===== 最近崩溃 =====\u0026#34; logcat -b crash -d | tail -20 # 模拟遥控器按键测试电视应用 PACKAGE=com.example.tvapp am start -n $PACKAGE/.MainActivity sleep 2 # 向下导航几次 input keyevent KEYCODE_DPAD_DOWN sleep 1 input keyevent KEYCODE_DPAD_DOWN sleep 1 input keyevent KEYCODE_DPAD_CENTER sleep 2 input keyevent KEYCODE_BACK sleep 1 # 一键降低系统动画速度（流畅度测试） settings put global window_animation_scale 0 settings put global transition_animation_scale 0 settings put global animator_duration_scale 0 # 快速清除应用数据并重启 pm clear com.example.app am start -n com.example.app/.MainActivity 附录：命令索引 命令 主要用途 包位置 am Activity/Service/Broadcast 控制 /system/bin/am pm 包管理、权限、安装卸载 /system/bin/pm dumpsys 系统服务状态查询 /system/bin/dumpsys settings 系统设置读写 /system/bin/settings wm 窗口参数管理 /system/bin/wm input 输入事件模拟 /system/bin/input service Binder 服务调用 /system/bin/service cmd 服务命令接口（替代 service call） /system/bin/cmd appops 细粒度权限管理 /system/bin/appops content ContentProvider 操作 /system/bin/content ime 输入法管理 /system/bin/ime monkey UI 压力测试 /system/bin/monkey svc 网络/电源控制 /system/bin/svc media 媒体播放控制 /system/bin/media getprop/setprop 系统属性 /system/bin/toolbox bugreport 诊断报告生成 /system/bin/bugreport top/ps 进程信息 /system/bin/toolbox dmesg 内核日志 /system/bin/toolbox logcat 日志查看 /system/bin/logcat strace 系统调用跟踪 /system/bin/strace 所有命令均在 adb shell 下执行。 其中 toolbox 是 Android 的嵌入式 busybox 替代品，包含 top/ps/dmesg/getprop 等基础命令。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/android-console%E5%91%BD%E4%BB%A4/","summary":"\u003ch1 id=\"android-framework-console-命令速查\"\u003eAndroid Framework Console 命令速查\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e适用场景：\u003ccode\u003eadb shell\u003c/code\u003e 下执行的 Android framework 层调试命令\n覆盖工具：am / pm / dumpsys / settings / wm / input / service / cmd / appops / content / ime / monkey / svc / media\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"目录\"\u003e目录\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#1-am--activity-manager\"\u003eam — Activity Manager\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#2-pm--package-manager\"\u003epm — Package Manager\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#3-dumpsys--%E7%B3%BB%E7%BB%9F%E6%9C%8D%E5%8A%A1%E8%AF%8A%E6%96%AD\"\u003edumpsys — 系统服务诊断\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#4-settings--%E7%B3%BB%E7%BB%9F%E8%AE%BE%E7%BD%AE\"\u003esettings — 系统设置\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#5-wm--window-manager\"\u003ewm — Window Manager\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#6-input--%E8%BE%93%E5%85%A5%E4%BA%8B%E4%BB%B6%E6%A8%A1%E6%8B%9F\"\u003einput — 输入事件模拟\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#7-service--service-%E8%B0%83%E7%94%A8\"\u003eservice — Service 调用\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#8-cmd--%E6%9C%8D%E5%8A%A1%E5%91%BD%E4%BB%A4%E6%8E%A5%E5%8F%A3\"\u003ecmd — 服务命令接口\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#9-appops--%E5%BA%94%E7%94%A8%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86\"\u003eappops — 应用权限管理\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#10-content--contentprovider-%E6%93%8D%E4%BD%9C\"\u003econtent — ContentProvider 操作\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#11-ime--%E8%BE%93%E5%85%A5%E6%B3%95%E7%AE%A1%E7%90%86\"\u003eime — 输入法管理\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#12-monkey--ui-%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95\"\u003emonkey — UI 压力测试\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#13-svc--%E7%BD%91%E7%BB%9C%E7%94%B5%E6%BA%90%E6%9C%8D%E5%8A%A1%E6%8E%A7%E5%88%B6\"\u003esvc — 网络/电源服务控制\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#14-media--%E5%A4%9A%E5%AA%92%E4%BD%93%E6%8E%A7%E5%88%B6\"\u003emedia — 多媒体控制\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#15-%E5%85%B6%E4%BB%96%E5%AE%9E%E7%94%A8%E5%91%BD%E4%BB%A4\"\u003e其他实用命令\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-am--activity-manager\"\u003e1. am — Activity Manager\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e管理 Activity、Service、Broadcast、Intent 的启动与调试。\u003c/p\u003e","title":"AndroidFrameworkConsole命令速查"},{"content":"3.2 Kernel 构建与设备树 学习日期: 2026-04-30 参考项目: Amlogic S905X5M (ross) Android 14 TV 前置知识: Linux 内核基础概念、C 语言、ARM64 架构\n1. 引言：Kernel 在 Android 中的角色 概念解释：Linux Kernel（内核）是操作系统的最底层核心，负责管理硬件资源（CPU、内存、外设）、提供进程调度、内存管理、网络协议栈、设备驱动等基础服务。在 Android 系统中，内核是唯一直接与硬件交互的软件层，Android 框架和应用程序的所有硬件的操作最终都要通过内核完成。\n代码体现：参考代码库使用内核版本 common14-5.15，即基于 Linux 5.15 主线内核的 Android Common Kernel。构建命令为：\n./mk ross -v common14-5.15 实际价值：没有内核，操作系统无法管理硬件资源。在嵌入式 Android 中，内核裁剪和驱动适配是 BSP（Board Support Package）的核心工作，直接影响系统的稳定性、性能和功能完整性。\n2. GKI（Generic Kernel Image） 2.1 概念解释 概念解释：GKI（通用内核映像）是 Google 在 Android 11/12 引入的架构，将内核分为两部分：\nGKI 内核（通用内核）：由 Google 维护，包含核心内核代码和与 SoC 无关的驱动。所有设备使用相同的内核二进制。 GKI 模块（厂商模块）：由 SoC 厂商（如 Amlogic）提供，以可加载内核模块（.ko）的形式存在，包含 SoC 特定的驱动。 GKI 的核心目标是实现内核与厂商驱动的解耦——SoC 厂商不需要 fork 内核源码，只需要提供与 GKI 接口兼容的模块。\n传统方式: Google 发布内核 → 厂商 fork 并修改 → 每次内核更新都要重新适配 GKI 方式: Google 发布 GKI 内核二进制 → 厂商提供兼容的 .ko 模块 → 内核更新不影响厂商驱动 2.2 代码体现 在参考代码库中，common/common14-5.15/ 目录是完整的 GKI 内核工程：\ncommon/common14-5.15/ ├── common/ # GKI 通用内核源码（主线 Linux + Android 补丁） │ ├── build.config.gki.aarch64 # GKI ARM64 构建配置 │ ├── build.config.amlogic # Amlogic 厂商构建配置 │ └── common_drivers/ # Amlogic 厂商驱动与设备树 └── driver_modules/ # 其他外设驱动模块（GPU、WiFi、Camera） Amlogic 构建配置（build.config.amlogic）：\n# 基于 GKI 通用配置 . ${ROOT_DIR}/${KERNEL_DIR}/build.config.gki.aarch64 # 使用 Amlogic 的 defconfig（最终 = gki_defconfig + amlogic_gki.fragment） DEFCONFIG=amlogic_gki_defconfig FRAGMENT_CONFIG=${KERNEL_DIR}/arch/arm64/configs/amlogic_gki.fragment # 合并配置 PRE_DEFCONFIG_CMDS=\u0026#34;.../scripts/kconfig/merge_config.sh -m -r \\ gki_defconfig amlogic_gki.fragment\u0026#34; # 编译目标 MAKE_GOALS=\u0026#34;Image modules Image.lz4 Image.gz dtbs\u0026#34; 构建产物：\n# ross-kernel/5.15/ 下为预编译产物 device/amlogic/ross-kernel/5.15/ ├── ross.dtb # ross 设备的 DTB ├── ross_soundbar.dtb # Soundbar 变体 DTB ├── ross_mxl258c.dtb # MXL258C 变体 DTB ├── dtbo.img # DTBO（设备树 overlay） ├── gki/boot-lz4.img # GKI 启动镜像 ├── gki/Image.gz # GKI 内核二进制 ├── vendor_boot.modules.load # vendor_boot 需要加载的模块列表 ├── vendor_dlkm.modules.load # vendor_dlkm 需要加载的模块列表 └── symbols/ # 调试符号 ├── vmlinux # 未压缩的内核映像（调试用） └── *.ko # 各个内核模块的调试符号 2.3 实际价值 GKI 解决了 Android 内核碎片化问题。在 GKI 之前，每个设备厂商 fork 内核后各自修改，导致：\n安全补丁需要厂商逐个移植，耗时数月 内核版本长期不更新，漏洞无法修复 Google 难以统一测试 GKI 让内核安全更新可以独立于厂商驱动进行。用户可以通过 Play Store 获取内核更新，而不需要等待厂商发布完整 OTA。\n3. Kernel 构建系统 3.1 defconfig 与 fragment 概念解释：\ndefconfig（默认配置）：内核的默认配置。Linux 内核有数千个可配置选项（驱动启用/禁用、功能开关、调试选项等）。defconfig 文件定义了特定平台/设备的基准配置集。内核编译时，make defconfig 根据这个文件生成最终的 .config。\nfragment（配置片段）：在 GKI 架构中引入的概念。为了保持 GKI 内核的通用性，Google 提供 gki_defconfig 作为通用的基准配置，SoC 厂商通过 fragment 文件添加自己的配置需求，两者通过 merge_config.sh 合并。\n代码体现：\n# merge_config.sh 将 gki_defconfig 和 amlogic_gki.fragment 合并 PRE_DEFCONFIG_CMDS=\u0026#34;scripts/kconfig/merge_config.sh -m -r \\ arch/arm64/configs/gki_defconfig \\ arch/arm64/configs/amlogic_gki.fragment\u0026#34; 实际价值：如果没有 defconfig/fragment 机制，SoC 厂商需要维护一份完整的配置文件副本，当上游 gki_defconfig 更新时，需要人工逐个比较差异并合并。fragment 机制让厂商只维护自己的增量部分，大大降低了内核配置的维护成本。\n3.2 构建流程 ./mk ross -v common14-5.15 │ ▼ mk.sh (common14-5.15/mk.sh) │ ├── 设置参数: KERNEL_DIR=common, BUILD_CONFIG=build.config.amlogic │ ├── Bazel 构建 (GKI 2.0): │ tools/bazel run //common:amlogic_dist │ │ 步骤: │ 1. merge_config.sh → 合并 gki_defconfig + amlogic_gki.fragment │ 2. make Image → 编译内核 │ 3. make modules → 编译内核模块 │ 4. make dtbs → 编译设备树 │ 5. lz4_compress → 压缩内核映像 │ 6. 复制产物到 dist 目录 │ └── Android 构建系统: ├── build_kernel_modules.mk → 组织内核模块到各分区 ├── boot.img 打包 → kernel + ramdisk + dtb ├── vendor_boot.img 打包 → vendor 内核模块 + dtbo └── dtb-avb.img 生成 → 带 AVB2 签名的 DTB 映像 Bazel：Google 的构建系统。common14-5.15 使用 Bazel 作为主构建引擎（tools/bazel run），替代了传统的 build/build.sh。Bazel 提供分布式构建、缓存和增量编译能力。\n3.3 内核压缩格式：lz4 概念解释：lz4 是一种高速压缩算法，压缩速度远快于 gzip（约 5-10 倍），解压缩速度极快（约 2-4 GB/s）。在嵌入式系统中，内核映像通常压缩存储以节省空间，启动时由 U-Boot 或内核自解压代码解压。\n代码体现：\n# build.config.amlogic 中的自定义压缩命令 function lz4_compress() { lz4 -f -12 --favor-decSpeed ${OUT_DIR}/arch/arm64/boot/Image \\ ${OUT_DIR}/arch/arm64/boot/Image.lz4 } -12 是最高压缩级别，--favor-decSpeed 优化解压速度。\n实际价值：Android 设备启动速度至关重要。使用 lz4 可以减少内核解压时间几百毫秒，对终端用户的\u0026quot;开机速度\u0026quot;感知有明显提升。相比 gzip，lz4 以稍大的压缩体积换取更快的解压速度。\n4. 设备树（Device Tree） 4.1 概念解释 概念解释：Device Tree（设备树）是一种描述硬件配置的数据结构，以树状层级描述 CPU 数量、内存地址范围、外设（I2C、SPI、UART、GPIO、显示、音频等）的寄存器地址、中断号、时钟频率等硬件信息。\n在 Linux 内核引入设备树之前，ARM Linux 内核需要为每一块开发板编写大量的 board-*.c 文件，在内核源码中硬编码硬件信息。这种做法导致内核源码急剧膨胀，且每块板子都需要重新编译内核。设备树将硬件描述从内核代码中分离出来，同一份内核二进制可以在不同硬件上运行，只需要更换设备树文件。\n关键格式：\n扩展名 格式 说明 .dts Device Tree Source 设备树源码，人类可读的文本格式 .dtsi Device Tree Source Include 设备树包含文件（类似 C 语言的 .h 头文件），定义 SoC 级别的共用硬件 .dtb Device Tree Blob 设备树二进制，编译后的设备树，U-Boot/内核实际使用的格式 .dtbo Device Tree Blob Overlay 设备树 overlay，动态覆盖/扩展基础 DTB 编译关系：\n.dts + .dtsi → DTC (Device Tree Compiler) → .dtb 4.2 DTS 基本语法（零基础入门，以 ross DTS 为例） 设备树语法基于节点（node）和属性（property），每个节点描述一个硬件，属性描述该硬件的参数。下面用 ross 产品实际使用的 s7d_s905x5m_bm201_4g.dts 逐步拆解。\n4.2.1 文件头与引用 // SPDX-License-Identifier: (GPL-2.0+ OR MIT) /dts-v1/; #include \u0026#34;meson-s7d.dtsi\u0026#34; #include \u0026#34;hdr10plus.dtsi\u0026#34; // 是注释，和 C 语言一样 /dts-v1/; 声明文件格式版本，固定写法 #include 像 C 语言一样引用其他文件。.dtsi 是可被包含的设备树文件（类似 .h 头文件）。SoC 级通用定义放在 .dtsi，板级特有内容放 .dts，这样同一颗芯片的不同板子不用重复写 SoC 部分 4.2.2 根节点（root node） / { amlogic-dt-id = \u0026#34;s7d_s905x5m_bm201-4g\u0026#34;; compatible = \u0026#34;s7d_s905x5m_bm201-4g\u0026#34;; interrupt-parent = \u0026lt;\u0026amp;gic\u0026gt;; #address-cells = \u0026lt;2\u0026gt;; #size-cells = \u0026lt;2\u0026gt;; // ... 子节点 }; / 代表根节点，整棵树的起点 属性 = 值; 描述硬件信息，常见值类型： 写法 含义 例子 \u0026quot;字符串\u0026quot; 字符串 compatible = \u0026quot;s7d_s905x5m_bm201-4g\u0026quot;; \u0026lt;数字\u0026gt; 32 位整数 #address-cells = \u0026lt;2\u0026gt;; \u0026lt;0x0 0x80000000\u0026gt; 整数数组 reg = \u0026lt;0x0 0x07400000 0x0 0x00100000\u0026gt;; \u0026amp;gic 引用另一个节点 interrupt-parent = \u0026lt;\u0026amp;gic\u0026gt;; 三个最核心的属性：\ncompatible——告诉内核\u0026quot;我是什么设备\u0026quot;，驱动靠它匹配。内核驱动注册时说\u0026quot;我能驱动 amlogic, meson-ir\u0026quot;，设备树写 compatible = \u0026quot;amlogic, meson-ir\u0026quot;，两者对上号驱动就开始工作 reg——硬件在内存中的地址。reg = \u0026lt;0x0 0x07400000 0x0 0x00100000\u0026gt; 因为 #address-cells = \u0026lt;2\u0026gt; 和 #size-cells = \u0026lt;2\u0026gt;（地址和大小都用 2 个 32 位整数表示），所以含义是：物理地址 0x07400000，大小 0x100000（1MB） status——启用/禁用：\u0026quot;okay\u0026quot; 或 \u0026quot;disabled\u0026quot; 4.2.3 节点嵌套——树状结构 / { ← 根节点 memory@00000000 { ← 子节点 device_type = \u0026#34;memory\u0026#34;; linux,usable-memory = \u0026lt;0x0 0x0 0x0 0xf0000000\u0026gt;; }; gpio_leds { ← 子节点 compatible = \u0026#34;gpio-leds\u0026#34;; pwr_led { ← 子节点的子节点 label = \u0026#34;pwr_led\u0026#34;; default-state = \u0026#34;on\u0026#34;; }; bt_led { label = \u0026#34;bt_led\u0026#34;; gpios = \u0026lt;\u0026amp;gpio GPIODV_6 GPIO_ACTIVE_HIGH\u0026gt;; }; }; }; 节点名后的 @00000000 表示该设备的起始地址。memory@00000000 = \u0026ldquo;内存从地址 0 开始\u0026rdquo;，ramoops@0x07400000 = \u0026ldquo;ramoops 在 0x07400000\u0026rdquo;。\n4.2.4 \u0026amp; 引用——在别处补充节点 \u0026amp;ir { status = \u0026#34;okay\u0026#34;; pinctrl-0 = \u0026lt;\u0026amp;remote_pins\u0026gt;; pinctrl-names = \u0026#34;default\u0026#34;; }; \u0026amp;ir 的意思是\u0026quot;找到名叫 ir 的节点，往里面加属性\u0026quot;。ir 节点定义在 SoC 级 .dtsi 中（meson-s7d.dtsi）：\n// meson-s7d.dtsi 中定义： ir: ir@8000 { compatible = \u0026#34;amlogic, meson-ir\u0026#34;; reg = \u0026lt;0x0 0xfe084080 0x0 0xA4\u0026gt;, \u0026lt;0x0 0xfe084000 0x0 0x58\u0026gt;; status = \u0026#34;disable\u0026#34; ← 默认关闭 protocol = \u0026lt;REMOTE_TYPE_NEC\u0026gt;; interrupts = \u0026lt;GIC_SPI 148 IRQ_TYPE_EDGE_RISING\u0026gt;; map = \u0026lt;\u0026amp;custom_maps\u0026gt;; }; 板级 DTS 用 \u0026amp;ir 把它打开并指定引脚：\n\u0026amp;ir { status = \u0026#34;okay\u0026#34;; ← 改为启用 pinctrl-0 = \u0026lt;\u0026amp;remote_pins\u0026gt;; }; 这种\u0026quot;基定义 + 板级补充\u0026quot;的模式是设备树的核心设计思想。\n4.2.5 标签（label）和引用 \u0026amp; ir: ir@8000 { ... } ir: 是标签，类似别名，方便其他地方引用 ir@8000 是节点名，@8000 是寄存器基地址 其他地方用 \u0026amp;ir 引用：\n\u0026amp;ir { status = \u0026#34;okay\u0026#34;; } // 往 ir 节点加属性 serial0 = \u0026amp;uart_B; // 引用 uart_B 作为串口0 interrupt-parent = \u0026lt;\u0026amp;gic\u0026gt;; // 引用 gic 中断控制器 4.2.6 aliases——给节点起短名 aliases { serial0 = \u0026amp;uart_B; serial1 = \u0026amp;uart_A; i2c0 = \u0026amp;i2c0; spi0 = \u0026amp;spi_nfc; }; 这样内核就知道\u0026quot;串口0\u0026quot;对应 uart_B，\u0026ldquo;I2C0\u0026quot;对应 i2c0。\n4.2.7 每个按键的完整 DTS 路径 以红外遥控器按键为例，看 DTS 怎么逐层描述硬件：\n// ① 根节点声明中断控制器 interrupt-parent = \u0026lt;\u0026amp;gic\u0026gt;; // ② SoC 级定义 IR 控制器（meson-s7d.dtsi） ir: ir@8000 { compatible = \u0026#34;amlogic, meson-ir\u0026#34;; reg = \u0026lt;0x0 0xfe084080 0x0 0xA4\u0026gt;; // 寄存器地址 interrupts = \u0026lt;GIC_SPI 148 IRQ_TYPE_EDGE_RISING\u0026gt;; // 中断148 protocol = \u0026lt;REMOTE_TYPE_NEC\u0026gt;; // NEC 协议 map = \u0026lt;\u0026amp;custom_maps\u0026gt;; // 指向按键映射表 }; // ③ 按键映射表（meson-ir-map.dtsi） custom_maps: custom_maps { map0 = \u0026lt;\u0026amp;map_0\u0026gt;; map_0: map_0 { customcode = \u0026lt;0xfb04\u0026gt;; // 遥控器地址码 keymap = \u0026lt; REMOTE_KEY(0x47, KEY_0) // 按键0x47 → KEY_0 REMOTE_KEY(0x1A, KEY_POWER) // 按键0x1A → KEY_POWER \u0026gt;; }; }; // ④ 板级使能 IR（s7d_s905x5m_bm201_4g.dts） \u0026amp;ir { status = \u0026#34;okay\u0026#34;; pinctrl-0 = \u0026lt;\u0026amp;remote_pins\u0026gt;; // GPIO 引脚 }; 内核启动后，驱动根据这些信息初始化 IR 硬件，用户按遥控器时驱动查 keymap 表，上报按键事件。\n4.2.8 看懂 #address-cells 和 #size-cells 这两个属性决定了 reg 中地址和大小各占几个 32 位整数：\n#address-cells = \u0026lt;2\u0026gt;; // 地址用 2 个 cell（64位） #size-cells = \u0026lt;2\u0026gt;; // 大小用 2 个 cell（64位） // 所以 reg = \u0026lt;0x0 0x07400000 0x0 0x00100000\u0026gt; // [地址高32位] [大小高32位] // [地址低32位] [大小低32位] // = 地址 0x07400000, 大小 0x100000 如果 #address-cells = \u0026lt;1\u0026gt;; #size-cells = \u0026lt;1\u0026gt;;（32位地址空间），则：\nreg = \u0026lt;0x07400000 0x00100000\u0026gt;; // 地址0x07400000, 大小0x100000 4.2.9 DTS 语法极简速查表 语法 含义 例子 /dts-v1/; 文件头 每个 DTS 第一行 #include \u0026quot;x.dtsi\u0026quot; 包含文件 #include \u0026quot;meson-s7d.dtsi\u0026quot; 节点名 { }; 定义硬件节点 cpu { } 标签:节点名@地址 节点加引用标签 ir: ir@8000 属性 = \u0026quot;字符串\u0026quot;; 字符串属性 status = \u0026quot;okay\u0026quot;; 属性 = \u0026lt;数字\u0026gt;; 整数属性 #address-cells = \u0026lt;2\u0026gt;; 属性 = \u0026lt;1 2 3\u0026gt;; 整数数组 reg = \u0026lt;0x0 0x80000000 0x0 0x100000\u0026gt;; 属性 = \u0026amp;标签; 引用节点 interrupt-parent = \u0026lt;\u0026amp;gic\u0026gt;; \u0026amp;标签 { }; 补充节点属性 \u0026amp;ir { status = \u0026quot;okay\u0026quot;; }; /* */ 或 // 注释 // 这是注释 4.2.10 一句话总结 设备树就是一份\u0026quot;硬件连接说明书\u0026rdquo;。SoC 厂家写 .dtsi 定义芯片内部有什么外设，板卡厂家写 .dts 定义板子上接了什么硬件，内核启动时读这份说明书来初始化驱动。\n4.3 代码体现 — S7D SoC 设备树层级 概念解释：Amlogic S905X5M（S7D 平台）的设备树采用三层结构：\n1. SoC 级 (.dtsi): meson-s7d.dtsi ← 定义 S7D 芯片的全部外设 2. SoC 子模块 (.dtsi): mesons7d_drm.dtsi ← DRM 显示子系统 mesons7d_audio.dtsi ← 音频子系统 mesons7d_cpufreq.dtsi ← CPU 频率调节 3. 板级 (.dts): s7d_s905x5m_bm201.dts ← 具体电路板配置 meson-s7d.dtsi（SoC 级）：定义 S7D 芯片所有 CPU 核心、外设控制器、中断控制器、时钟、引脚复用等，位于：\ncommon/common14-5.15/common/common_drivers/arch/arm64/boot/dts/amlogic/meson-s7d.dtsi 内容包含：\n4 核 Cortex-A55 CPU（通过 PSCI 启停） GIC（Generic Interrupt Controller）：中断控制器 timer：ARM 架构计时器 uart_A/B/C/D/E：5 个 UART 串口 i2c0-4：5 个 I2C 控制器 spicc0/spi_nfc：SPI 控制器 sd_emmc_a/b/c：SD/eMMC 控制器 ethmac：以太网 MAC clkc：时钟控制器 pinctrl：引脚功能复用 概念解释——GIC（Generic Interrupt Controller）：ARM 架构的标准中断控制器，负责将外设的中断信号路由到 CPU 核心。支持 SPI（共享外设中断）、PPI（私有外设中断，每个 CPU 核心独立）、LPI（本地特定中断）三种类型。\n概念解释——PSCI（Power State Coordination Interface）：ARM 定义的电源管理接口标准，由 BL31（ATF）实现。内核通过 SMC（Secure Monitor Call）指令调用 PSCI 来管理 CPU 的启停和休眠，如 CPU 热插拔、suspend/resume。\ns7d_s905x5m_bm201.dts（板级）：ross 设备的具体配置，位于：\ncommon/common14-5.15/common/common_drivers/arch/arm64/boot/dts/amlogic/s7d_s905x5m_bm201.dts 通过 #include 包含 SoC 级 DTSI，然后覆盖或添加板级特有内容：\n内存布局：2GB DDR（reg = \u0026lt;0x0 0x0 0x0 0x80000000\u0026gt;） 保留内存：为各种子系统预留内存区域 外设引脚配置：GPIO 引脚功能选择（如 I2C 用哪组引脚） 电源调节器（regulator）：板级电压定义 音频路由：codec、DAI link 配置 WiFi/BT：BCM 模块的 GPIO 和电源配置 热管理：thermal zones、降温策略 4.4 DTB 的加载流程 编译时: .dts + .dtsi → DTC → .dtb （内核编译时生成） .dtb + overlay → dtc → .dtbo 启动时: U-Boot 从 dtb 分区读取 dtb → 传递给内核 内核解析 DTB → 枚举设备 → 加载对应驱动 或（vendor_boot 方式）: boot.img 包含内核 → vendor_boot.img 包含 DTB + DTBO U-Boot 加载两者到内存 → 内核合并 DTB 和 DTBO → 枚举设备 DTBO（Device Tree Overlay）：允许在不修改主 DTB 的情况下动态添加或覆盖设备节点。在 Android 中用于分离 SoC DTB 和板级差异，当同一 SoC 用于不同产品时，只需更换 DTBO 而不需要重新编译内核。\n代码体现：\n# BoardConfig.mk BOARD_PREBUILT_DTBOIMAGE := $(DEVICE_PRODUCT_PATH)-kernel/$(TARGET_KERNEL_DIR)/dtbo.img device/amlogic/ross-kernel/5.15/dtbo.img 是 ross 设备的 DTBO 预编译映像。\n4.5 实际价值 设备树是嵌入式 Linux 开发的基石。没有设备树：\n每块新电路板都需要修改内核源码，增加大量 board-*.c 文件 编译一次内核只能支持一种硬件 硬件配置变更（如调整内存大小、更换 WiFi 模块）都需要重新编译内核 Android 生态碎片化将更加严重 5. Reserved Memory（保留内存） 概念解释：reserved-memory（保留内存）是指在系统总内存中划出一部分，预留给特定硬件或软件功能使用。内核和用户空间不能随意使用这些内存区域。在嵌入式 Android 中，保留内存用于：\n安全监控（secmon）：TEE 安全世界使用的内存 显存（ion/CMA）：GPU 和显示系统使用的连续物理内存 编解码器（codec_mm）：视频编解码使用的内存 调试（ramoops）：保存内核 panic 日志 安全视频解码（secure_vdec）：DRM 内容解密后的帧缓存 代码体现（从 s7d_s905x5m_bm201.dts）：\nreserved-memory { #address-cells = \u0026lt;2\u0026gt;; #size-cells = \u0026lt;2\u0026gt;; ranges; /* 内核崩溃日志保留区域 — ramoops */ ramoops@0x07400000 { compatible = \u0026#34;ramoops\u0026#34;; reg = \u0026lt;0x0 0x07400000 0x0 0x00100000\u0026gt;; // 1MB record-size = \u0026lt;0x20000\u0026gt;; // 128KB 主记录 console-size = \u0026lt;0x40000\u0026gt;; // 256KB console 日志 ftrace-size = \u0026lt;0x80000\u0026gt;; // 512KB ftrace 跟踪 pmsg-size = \u0026lt;0x10000\u0026gt;; // 64KB 用户空间消息 }; /* TEE 安全监控内存 */ secmon_reserved:linux,secmon { no-map; // 不建立页表映射（安全世界独占） reg = \u0026lt;0x0 0x05000000 0x0 0x2400000\u0026gt;; // 36MB }; /* CMA：用于连续内存分配 */ dmaheap_cma_reserved:heap-gfx { compatible = \u0026#34;shared-dma-pool\u0026#34;; reusable; // 未使用时可被内核使用 size = \u0026lt;0x0 0x5800000\u0026gt;; // 88MB（给 GPU 图形用） }; /* 视频编解码 CMA */ codec_mm_cma:linux,codec_mm_cma { reusable; size = \u0026lt;0x0 0x16C00000\u0026gt;; // 364MB（CTS 测试最大场景） }; /* 安全视频解码 */ secure_vdec_reserved:linux,secure_vdec_reserved { no-map; size = \u0026lt;0x0 0x1000000\u0026gt;; // 16MB }; }; 概念解释：\nCMA（Contiguous Memory Allocator）：内核的一种内存分配机制，专门用于分配大块连续物理内存。嵌入式设备中的 GPU、视频编解码、相机等硬件 DMA 引擎需要连续的物理内存，而标准内核内存分配器（buddy allocator）容易导致碎片化。CMA 预留一个大区域，在未被硬件占用时可被内核普通页面使用，硬件需要时再回收——兼顾了内存利用率和分配需求。\nion：Android 的内存分配器框架，提供多种堆（heap）类型用于不同硬件模块（显存、多媒体、系统）。ion_cma_reserved 是 CMA 类型的堆。\nramoops：内核崩溃时，在保留内存中保存最后一次的日志（console log、ftrace 等）。重启后可以通过 dmesg 或 pstore 文件系统读取上次崩溃的信息。对嵌入式设备调试极度重要——因为设备通常没有串口连接。\nno-map：DTS 中的标志，指示内核不要为这片内存建立页表映射。用于安全世界独占的内存（如 TEE），普通世界（包括 Linux 内核）无权访问。\n实际价值：保留内存机制是嵌入式系统内存管理的关键。如果没有合理的保留内存分配：\n视频解码找不到足够的连续内存，导致播放卡顿 内核崩溃后日志丢失，无法定位问题 TEE 安全内存被普通世界访问，安全防线被突破 GPU 运行出现内存碎片导致的停滞 6. 内核模块（Kernel Modules） 6.1 概念解释 概念解释：内核模块（.ko = kernel object）是可以在不重新编译内核的情况下动态加载到内核中的代码片段。它实现了设备驱动、文件系统、协议栈等功能，但以独立文件形式存在。需要时用 insmod 或 modprobe 加载，不需要时用 rmmod 卸载。\n在 GKI 架构下，SoC 厂商的驱动都以模块形式提供，不直接编译进内核映像。\n6.2 代码体现 模块加载列表（device/amlogic/ross-kernel/5.15/vendor_boot.modules.load）：\namlogic-gkitool.ko amlogic-debug-iotrace.ko amlogic-uart.ko amlogic-hwspinlock.ko amlogic-debug.ko amlogic-secmon.ko amlogic-cpuinfo.ko page_trace.ko aml_cma.ko clk-scmi.ko amlogic-clk.ko amlogic-clk-soc-s7d.ko amlogic-aoclk-soc-t5w.ko amlogic-gpio.ko amlogic-pinctrl-soc-s7d.ko amlogic-mailbox.ko amlogic-pwm.ko ... 模块分类：\n类别 模块 功能 时钟 amlogic-clk.ko, clk-scmi.ko SoC 时钟控制 GPIO amlogic-gpio.ko GPIO 控制 引脚复用 amlogic-pinctrl-soc-s7d.ko SoC 引脚功能配置 安全监控 amlogic-secmon.ko 安全世界通信接口 UART amlogic-uart.ko 串口驱动 调试 amlogic-debug.ko 内核调试支持 DDR amlogic-memory-debug.ko 内存调试 CMA aml_cma.ko CMA 内存分配器管理 GKI 模块加载顺序（vendor_dlkm.modules.load）：\n模块按依赖顺序排列，modprobe 按列表顺序加载。\n6.3 构建集成 # build_kernel_modules.mk # 读取 modules.load 列表 RAMDISK_KERNEL_MODULES_LOAD := $(shell cat \\ $(DEVICE_PRODUCT_PATH)-kernel/$(TARGET_KERNEL_DIR)/vendor_boot.modules.load) # 将模块打包进 vendor_boot 的 ramdisk BOARD_VENDOR_RAMDISK_KERNEL_MODULES ?= $(RAMDISK_KERNEL_MODULES) BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD ?= $(RAMDISK_KERNEL_MODULES_LOAD) 6.4 实际价值 内核模块机制让嵌入式系统更加灵活：\n不需要的驱动可以不加载，节省内存和启动时间 问题驱动可以单独替换，不需要重新编译整个内核 GKI 架构下，模块是厂商定制内核的唯一方式——没有模块机制，GKI 就不可能实现 7. Boot Image 打包架构 7.1 boot.img 结构 概念解释：boot.img 是 Android 的启动映像，包含内核和初始根文件系统（ramdisk）。U-Boot 将 boot.img 加载到内存后，内核从 ramdisk 启动 init 进程。\nboot.img ├── kernel ← 内核映像 (Image.lz4) ├── ramdisk ← 初始根文件系统（init 和启动脚本） ├── dtb ← 设备树（部分配置中在 boot.img 末尾） └── boot header ← 启动头（内核加载地址、ramdisk 地址等） 代码体现：\n# kernel_config_build.mk KERNEL_ROOTDIR := common BOARD_MKBOOTIMG_ARGS := --kernel_offset $(BOARD_KERNEL_OFFSET) \\ --header_version $(BOARD_BOOT_HEADER_VERSION) 7.2 vendor_boot.img 概念解释：vendor_boot.img 是 Android 13+ 引入的启动映像，用于将厂商特定的内核模块和 DTB 与通用 boot.img 分离。这使得 GKI 内核映像（boot.img）可以保持通用，而厂商定制内容放在 vendor_boot.img 中。\nvendor_boot.img ├── vendor ramdisk ← 厂商内核模块 (.ko) ├── dtb ← 设备树二进制（厂商板级 DTB） └── dtbo ← DTBO overlay 7.3 DTB 分区 概念解释：在 Amlogic 平台中，DTB 可以存储在独立分区（dtb 分区）或内嵌到 boot.img/vendor_boot.img 中。独立 dtb 分区的好处是：更换设备树映像不需要重新烧录整个 boot。\n# build_kernel_modules.mk # 将 DTB 打包为 dtb-avb.img（带 AVB hashtree footer） $(INSTALLED_BOARDDTB_TARGET): $(LOCAL_DTB) cp $(LOCAL_DTB) $@ $(AVBTOOL) add_hash_footer \\ --partition_size $(BOARD_DTBIMAGE_PARTITION_SIZE) \\ --partition_name dt 7.4 各分区对应关系 分区 内容 来源 boot kernel + generic ramdisk GKI 预编译 vendor_boot vendor modules + DTB + DTBO ross-kernel/5.15/ dtb 设备树（独立分区） ross.dtb 等 dtbo DTBO overlay dtbo.img vendor_dlkm 动态加载的 vendor 模块 ko 文件 + modules.load 8. Kernel Command Line 概念解释：内核命令行（kernel cmdline）是在内核启动时传递给内核的参数字符串，通过 U-Boot 的 bootargs 环境变量设置。内核解析这些参数来配置启动行为，如 console 输出、SELinux 模式、init 路径、分区信息等。\n代码体现（BoardConfig.mk）：\nBOARD_KERNEL_CMDLINE += bootconfig BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive # SELinux 宽松模式（开发版） BOARD_KERNEL_CMDLINE += androidboot.serialno=$(SERIAL) BOARD_KERNEL_CMDLINE += firmware_class.path=/vendor/etc/firmware # Boot console 为 ttyS0，波特率 921600 BOARD_KERNEL_CMDLINE += console=ttyS0,921600 含义：\nandroidboot.selinux=permissive：SELinux 在 permissive 模式运行，只记录不阻止违规操作（开发调试用） console=ttyS0,921600：串口控制台，用于调试日志输出 bootconfig：启用 Boot Configuration（一种增强内核参数传递机制） 9. 板级变体与 DTB ross 设备有多种硬件变体，每种使用不同的 DTB：\n产物的 DTB 来源（KERNEL_DEVICETREE） 对应产品 ross.dtb s7d_s905x5m_bm201.dtb + s7d_s905x5m_bm202.dtb + s7d_s905x5m_bm209.dtb + s7d_s905x5m_bm201_4g.dtb 四合一 ross (S905X5M) 全系列 ross_soundbar.dtb s7d_s905x5m_bm201_soundbar.dtb ross Soundbar 版 ross_mxl258c.dtb s7d_s905x5m_bm201_mxl258c.dtb ross MXL258C 版 对应板级 DTS 源码路径：\ncommon/.../dts/amlogic/s7d_s905x5m_bm201.dts common/.../dts/amlogic/s7d_s905x5m_bm202.dts common/.../dts/amlogic/s7d_s905x5m_bm209.dts common/.../dts/amlogic/s7d_s905x5m_bm201_4g.dts ← 当前 ross 4G 版 common/.../dts/amlogic/s7d_s905x5m_bm201_soundbar.dts common/.../dts/amlogic/s7d_s905x5m_bm201_mxl258c.dts 9.1 ross.dtb 实际是多 DTB 合并镜像 在 common/project/amlogic/ross/build.config.meson.arm64.trunk.5.15 中定义了：\nKERNEL_DEVICETREE=\u0026#34;s7d_s905x5m_bm201 s7d_s905x5m_bm202 s7d_s905x5m_bm209 s7d_s905x5m_bm201_4g\u0026#34; KERNEL_DEVICETREE_SOUNDBAR=\u0026#34;s7d_s905x5m_bm201_soundbar\u0026#34; KERNEL_DEVICETREE_FCC_PIP=\u0026#34;s7d_s905x5m_bm201_mxl258c\u0026#34; KERNEL_DEVICETREE 包含 4 个 DTS 目标，编译出 4 个独立 DTB。构建脚本 build_kernel_5.15.sh 检测到 dtb_files_count \u0026gt; 1，使用 dtbTool 将 4 个 DTB 合并为单一 ross.dtb：\n# build_kernel_5.15.sh L218-L219 ${DTBTOOL_DIR}/dtbTool -o ${DEVICE_KERNEL_DIR}/${BOARD_DEVICENAME}.dtb \\ -p ${DTBTOOL_DIR}/ ${OUT_AMLOGIC_DIR}/dtb/ U-Boot 启动时通过硬件检测（如内存大小或 GPIO 电平）自动匹配合适的 DTB 加载，一份 ross.dtb 覆盖 4 种硬件变种。当前 ross 产品实际对应 s7d_s905x5m_bm201_4g.dts（4GB 内存）。\n10. 流程图汇总 Kernel 构建与集成流程 内核源码构建（common14-5.15/） ┌─────────────────────────────────────────────────┐ │ gki_defconfig + amlogic_gki.fragment │ │ ↓ merge_config.sh │ │ .config (完整配置) │ │ ↓ make │ │ ┌──────┼──────────┐ │ │ ↓ ↓ ↓ │ │ Image modules dtbs │ │ ↓ lz4 ↓ ↓ │ │ Image.lz4 .ko s7d_s905x5m_bm201.dtb │ └──────┬──────┬──────────┬────────────────────────┘ │ │ │ ▼ ▼ ▼ Android 构建系统集成 ┌─────────────────────────────────────────────────┐ │ boot.img ← Image.lz4 + generic ramdisk │ │ vendor_boot.img ← .ko + dtb + dtbo │ │ dtb.img ← dtb (with AVB footer) │ │ │ │ 最终产物: │ │ out/target/product/ross/ │ │ ├── boot.img │ │ ├── vendor_boot.img │ │ ├── dtb-avb.img │ │ └── vendor/lib/modules/*.ko │ └─────────────────────────────────────────────────┘ 设备树层级关系 meson-s7d.dtsi ← SoC 级（S7D 芯片所有外设） ├── mesons7d_drm.dtsi ← DRM 显示子系统 ├── mesons7d_audio.dtsi ← 音频子系统 ├── mesons7d_cpufreq.dtsi← CPU 频率调节 └── firmware_ab.dtsi ← A/B 系统固件区域 s7d_s905x5m_bm201.dts ← 板级（ross 设备） └── #include meson-s7d.dtsi ├── memory: 2GB DDR ├── reserved-memory: secmon/ramoops/CMA ├── regulator: 电压调节器定义 ├── audio: I2S/SPDIF/PCM 路由 ├── WiFi/BT: BCM 模块 GPIO └── thermal: 散热策略 11. 关键代码文件索引 功能 路径 GKI 内核工程根目录 common/common14-5.15/ Amlogic 构建配置 common/common14-5.15/common/build.config.amlogic Amlogic defconfig fragment common/common14-5.15/common/common_drivers/arch/arm64/configs/amlogic_gki.fragment S7D SoC 级设备树 common/common14-5.15/common/common_drivers/arch/arm64/boot/dts/amlogic/meson-s7d.dtsi ross 板级设备树 common/common14-5.15/common/common_drivers/arch/arm64/boot/dts/amlogic/s7d_s905x5m_bm201.dts 内核构建脚本 common/common14-5.15/mk.sh 内核模块集成 device/amlogic/common/build_kernel_modules.mk ross 内核预编译产物 device/amlogic/ross-kernel/5.15/ ross 内核配置 device/amlogic/ross/kernel_config_build.mk BoardConfig (kernel 部分) device/amlogic/ross/BoardConfig.mk 12. 自问自答 Q: GKI 和传统内核构建的主要区别？ A: 传统方式厂商 fork 整个内核源码修改，GKI 方式厂商只提供内核模块和设备树，通用内核直接用 Google 预编译的 GKI 映像。区别类似于\u0026quot;从头写一个类 vs 继承并重写几个方法\u0026quot;——GKI 大幅降低了厂商维护成本。\nQ: DTS 和 DTB 是什么关系？ A: DTS 是源码（文本），DTB 是编译后的二进制文件（机器可读）。类比：.c → 编译器 → .elf。内核启动时解析的是 DTB，不是 DTS。所以烧录到设备上的是 .dtb 文件。\nQ: 为什么需要保留内存？直接 malloc 不行吗？ A: 不行。普通应用程序通过 malloc 分配的内存是虚拟地址，物理内存可能不连续。但 DMA 硬件（GPU、视频编解码）需要物理连续的内存，这就是 CMA 保留内存的作用。此外，安全世界（TEE）用的内存不能给 Linux 使用，也需要预先保留。\nQ: ramoops 怎么用？ A: 设备内核崩溃后重启，进入系统后执行 cat /sys/fs/pstore/console-ramoops-0 查看上次崩溃前的内核日志。这对没有串口的量产设备调试非常有用。\nQ: 一个 SoC 多个产品怎么管理 DTB？ A: 通用部分放在 SoC 级 .dtsi，差异部分放在板级 .dts。更灵活的方式是使用 DTBO——SoC DTB 通用，不同产品的差异通过 DTBO overlay 叠加。这样只需要维护一份 SoC DTB 和多个小 DTBO。\nQ: vendor_boot 和 boot 分开了，好处是什么？ A: 厂商更新驱动只需要替换 vendor_boot 分区，boot 分区（GKI 内核）保持不变。这对安全更新意义重大——Google 可以通过 Play Store 推送 GKI 内核更新而不影响厂商的驱动。\n13. 下一步 理解内核构建设备树后，建议继续深入：\nHAL 与 Treble（notes/09-hal-treble-hidl接口.md）：理解内核之上 Android 如何抽象硬件 SELinux（notes/10-selinux-安全策略.md）：理解内核安全子系统 深入驱动：选择一个 Amlogic 驱动模块（如 amlogic-pinctrl 或 amlogic-secmon），阅读源码理解内核驱动开发模式 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/08-kernel-%E8%AE%BE%E5%A4%87%E6%A0%91/","summary":"\u003ch1 id=\"32-kernel-构建与设备树\"\u003e3.2 Kernel 构建与设备树\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e学习日期: 2026-04-30\n参考项目: Amlogic S905X5M (ross) Android 14 TV\n前置知识: Linux 内核基础概念、C 语言、ARM64 架构\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-引言kernel-在-android-中的角色\"\u003e1. 引言：Kernel 在 Android 中的角色\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e概念解释\u003c/strong\u003e：Linux Kernel（内核）是操作系统的最底层核心，负责管理硬件资源（CPU、内存、外设）、提供进程调度、内存管理、网络协议栈、设备驱动等基础服务。在 Android 系统中，内核是\u003cstrong\u003e唯一直接与硬件交互\u003c/strong\u003e的软件层，Android 框架和应用程序的所有硬件的操作最终都要通过内核完成。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代码体现\u003c/strong\u003e：参考代码库使用内核版本 \u003cstrong\u003ecommon14-5.15\u003c/strong\u003e，即基于 Linux 5.15 主线内核的 Android Common Kernel。构建命令为：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e./mk ross -v common14-5.15\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e实际价值\u003c/strong\u003e：没有内核，操作系统无法管理硬件资源。在嵌入式 Android 中，内核裁剪和驱动适配是 BSP（Board Support Package）的核心工作，直接影响系统的稳定性、性能和功能完整性。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-gkigeneric-kernel-image\"\u003e2. GKI（Generic Kernel Image）\u003c/h2\u003e\n\u003ch3 id=\"21-概念解释\"\u003e2.1 概念解释\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e概念解释\u003c/strong\u003e：GKI（通用内核映像）是 Google 在 Android 11/12 引入的架构，将内核分为两部分：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGKI 内核（通用内核）\u003c/strong\u003e：由 Google 维护，包含核心内核代码和与 SoC 无关的驱动。所有设备使用相同的内核二进制。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGKI 模块（厂商模块）\u003c/strong\u003e：由 SoC 厂商（如 Amlogic）提供，以可加载内核模块（.ko）的形式存在，包含 SoC 特定的驱动。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eGKI 的核心目标是实现\u003cstrong\u003e内核与厂商驱动的解耦\u003c/strong\u003e——SoC 厂商不需要 fork 内核源码，只需要提供与 GKI 接口兼容的模块。\u003c/p\u003e","title":"08-3.2Kernel构建与设备树"},{"content":"3.1 Bootloader：U-Boot + 安全启动 学习日期: 2026-04-30 参考项目: Amlogic S905X5M (ross) Android 14 TV 前置知识: 嵌入式系统启动流程、ARM TrustZone 基本概念\n1. 引言：Bootloader 是什么？ 概念解释：Bootloader（启动加载器）是设备上电后第一个执行的软件。它的职责是初始化硬件（时钟、DDR 内存、存储控制器）、从存储介质（eMMC/NAND）加载操作系统映像到内存，然后跳转执行。在 Android 嵌入式系统中，bootloader 不仅要\u0026quot;启动系统\u0026quot;，还要验证启动链的完整性，防止被篡改的系统运行。\n代码体现：参考代码库中，bootloader 源码位于 bootloader/uboot-repo/，构建命令为：\n./mk s7d_bm201 --avb2 --vab # 或针对 ross 设备： cd bootloader/uboot-repo \u0026amp;\u0026amp; ./mk s7d_bm201 --update-bl2 --update-bl2e --update-bl2x --update-bl31 --update-bl32 --avb2 --vab 实际价值：没有 bootloader，芯片上电后 CPU 不知道从哪里取指令、内存没有初始化、没有任何代码可以执行。在 Android 安全模型中，bootloader 是第一道防线——如果 bootloader 不可信，后续整个系统都不可信。\n2. ARM 处理器启动流程基础 在深入 Amlogic U-Boot 之前，需要理解 ARM 处理器的启动模式：\n2.1 ARM TrustZone 概念解释：TrustZone 是 ARM 架构的硬件安全扩展，将 SoC 内部资源划分为两个世界：\nNormal World（普通世界）：运行 Linux/Android 等富操作系统 Secure World（安全世界）：运行可信执行环境（TEE），处理密钥、认证、DRM 等敏感操作 两个世界通过**监控模式（Monitor Mode）**切换，由硬件强制隔离。\n2.2 ARM 启动时安全等级 ARM 处理器启动时默认处于 Secure World。bootloader 的前几个阶段（BL1/BL2）在安全世界中执行，确保安全启动链从头建立。只有当安全启动链路验证通过后，才会跳转到 Normal World 启动 Android。\n2.3 Amlogic S905X5M 的处理器架构 S905X5M 基于 ARM 大小核架构：\nCortex-A76：大核（高性能） Cortex-A55：小核（低功耗） 使用 ARMv8.2-A 架构，支持 64 位运算。多核心间的调度管理由内核的 HMP（Heterogeneous Multi-Processing）或 EAS（Energy-Aware Scheduling）处理。\n3. U-Boot 是什么？ 概念解释：U-Boot（Universal Bootloader）是嵌入式 Linux 系统中最主流的开源 bootloader，支持 ARM、RISC-V、x86 等多种架构。它提供：\n硬件初始化（DDR、时钟、存储、网络） 命令行交互界面（类似小型 OS 的 shell） 从多种介质引导内核（eMMC、NAND、USB、TFTP） 支持脚本化和环境变量配置 代码体现：Amlogic 参考代码库使用 U-Boot v2019（位于 bootloader/uboot-repo/bl33/v2019/），这是一个基于 2019 年主线 U-Boot 的 Amlogic 定制版本。同时仓库也保留了 v2015、v2023、v2025 版本用于不同芯片。\n实际价值：在 Android 嵌入式系统中，U-Boot 负责最终加载 Android 内核（boot.img）并启动系统。Amlogic 在主线 U-Boot 基础上增加了大量定制功能：AVB2 验证、VAB A/B 槽控制、fastboot 下载、分区管理等。\n4. Amlogic 多级启动链（Boot Flow） 这是 Amlogic 芯片独有的启动架构，不同于一般 ARM 平台的两级启动（Boot ROM → U-Boot），Amlogic 将启动过程拆分为 5 个阶段：\n4.1 总体架构 Boot ROM (芯片固话) ↓ [加载到 SRAM] BL2 (bootloader 第一阶段) ← 安全世界 ↓ BL30 (SCP 协处理器固件) ← 电源管理 ↓ BL31 (ARM Trusted Firmware) ← 安全监控器 ↓ BL32 (TEE 可信执行环境) ← 可选，安全服务 ↓ BL33 (U-Boot) ← 普通世界 ↓ boot.img (Linux Kernel + ramdisk) 4.2 各阶段详解 BL2 — 第一阶段 Bootloader 概念解释：BL2（Boot Loader stage 2）是芯片 Boot ROM 之后执行的第一段用户代码。它运行在 SRAM（片上静态内存）中，负责：\n初始化 DDR 内存控制器 加载后续各阶段（BL30/BL31/BL32/BL33）到 DDR 内存 验证各阶段固件的签名（安全启动启用时） 代码体现：BL2 源代码在 bootloader/uboot-repo/bl2/ 目录，构建后生成 bl2.bin.sto（用于 eMMC 启动）和 bl2.bin.usb（用于 USB 烧录启动）。\n构建流程在 fip/s7d/build.sh 中通过 acpu-imagetool 工具对 BL2/BL2E/BL2X 进行签名和打包：\nacpu-imagetool create-boot-blobs \\ --chipset-authen-algorithm=rsa,none \\ --device-authen-algorithm=rsa,none \\ --infile-bl2-payload=bl2.bin.sto \\ --scs-family=s7d \\ --outfile-bb1st=${output}/bb1st.sto.bin 实际价值：BL2 是整个安全启动链的\u0026quot;信任根\u0026quot;（Root of Trust）之后的第一个用户代码。如果 BL2 本身被篡改，整个设备就不安全了。因此 BL2 通常由 Boot ROM 使用芯片熔丝（efuse）中的公钥哈希验证。\nBL30 — SCP 协处理器固件 概念解释：BL30 是运行在 SCP（System Control Processor，系统控制协处理器）上的固件。SCP 是一个独立的微控制器内核（Cortex-M 系列），专门负责：\n电源管理（DVFS、休眠唤醒） 时钟门控和复位控制 系统热管理 代码体现：BL30 构建脚本在 fip/build_bl30.sh，生成的固件由 BL2 加载到 SCP 专用的内存空间。\n实际价值：将电源管理独立到 BL30/SCP 上，主 CPU（Cortex-A76/A55）可以在空闲时彻底休眠，由 SCP 处理低功耗任务，这对 Android TV 的遥控器唤醒、待机功耗至关重要。\nBL31 — ARM Trusted Firmware (ATF) 概念解释：BL31 运行在 Secure World 的监控模式，是 ARM Trusted Firmware 的核心组件。它实现：\nSMC（Secure Monitor Call）处理：普通世界和安全世界之间的网关 PSCI（Power State Coordination Interface）：CPU 电源管理接口（CPU 开启/关闭/休眠） 安全中断路由：确保安全中断直达 Secure World 代码体现：参考代码库支持两个 BL31 版本——v1.3 和 v2.7，位于 bootloader/uboot-repo/bl31/。构建脚本 fip/build_bl31.sh 根据不同 SoC 选择版本。\n构建命令中 --update-bl31 参数指定需要重新构建 BL31。\n实际价值：BL31 是 Android 安全模型在固件层的支柱。Linux 内核通过 PSCI 接口（如 cpu_on/cpu_off）管理 CPU 核心，而这些接口的实际实现由 BL31 提供。没有 BL31，Android 就无法管理多核 CPU 的电源状态。\nBL32 — TEE 可信执行环境 概念解释：BL32 是运行在 Secure World 中的 TEE（Trusted Execution Environment）操作系统，常见的有：\nOP-TEE（Open Portable Trusted Execution Environment，开源） Trusty（Google 的 TEE 实现） TEE 提供在安全世界中运行可信应用（TA，Trusted Application）的能力，处理：\nDRM 解密（Widevine L1） 指纹/人脸识别数据比对 支付密钥管理 AVB 回滚索引的存储 代码体现：OP-TEE 的集成在 bootloader/uboot-repo/bl32/。构建时通过 CONFIG_NEED_BL32 控制是否包含 BL32。U-Boot 端有 OP-TEE 驱动支持 AVB TA（Trusted Application）。\n实际价值：没有 TEE，敏感操作（如 DRM 解密、支付认证）必须在普通世界进行，攻击者可以通过 root 提权窃取密钥。TEE 通过硬件隔离确保即使 Linux 内核被攻破，密钥也不会泄露。\nBL33 — U-Boot BL33 就是前面介绍的 U-Boot，运行在普通世界。它是整个 bootloader 的最后阶段，负责加载 Android 系统。\n4.3 FIP 打包 概念解释：FIP（Firmware Image Package）是 ARM Trusted Firmware 定义的一种固件打包格式，将 BL2、BL30、BL31、BL32、BL33 等多个固件镜像打包成一个文件，方便烧录和管理。\n代码体现：构建产物输出到 fip/_tmp/ 目录，通过 package 函数打包成最终的 u-boot.bin.signed。最终生成的 bootloader 烧录到 bootloader 分区的头部。\n# build_uboot 函数调用链： pre_build_uboot → init_variable_early → build_uboot → build_blx → package → copy_bootloader 实际价值：FIP 提供标准化的多固件打包方式，Boot ROM 只需要加载 BL2，BL2 从 FIP 包中解析并加载其余阶段。\n5. AVB2 (Android Verified Boot 2.0) 5.1 概念解释 概念解释：AVB2（Android Verified Boot 2.0）是 Google 在 Android 中引入的安全启动验证框架。它的核心目标是确保设备上运行的系统镜像与出厂时签名的一致，任何篡改都会被检测到。\nAVB2 的工作方式是在每个分区（boot、system、vendor 等）的末尾添加加密认证信息（hash footer 或 hashtree footer）。启动时逐层验证：\nvbmeta (最顶层签名) ├── boot.img hash ← 验证内核是否篡改 ├── system.img hashtree ← 验证系统文件是否篡改 ├── vendor.img hashtree └── product.img hashtree 5.2 核心概念 vbmeta：AVB2 的核心是一个名为 vbmeta 的小分区，它包含：\n所有已验证分区的 hash/哈希树根 公钥（或公钥的 hash） 验证模式：ENFORCE（强制验证，无法启动被篡改的系统）、WARNING（警告但允许启动）、LOCKED（设备锁定状态，禁止刷写） 链式信任：\n出厂熔断的公钥 hash → vbmeta 签名 → boot.img hash → Kernel → system.img hashtree → vendor.img hashtree 回滚保护（Rollback Protection）：\n每个分区都有一个回滚索引（rollback index） 当系统 OTA 升级后，回滚索引递增 无法启动回滚索引比当前值低的旧系统——防止攻击者刷回旧版本利用已知漏洞 5.3 代码体现 BoardConfig.mk 中的 AVB 配置：\n# device/amlogic/ross/BoardConfig.mk BOARD_AVB_ENABLE := true # 启用 AVB2 BOARD_AVB_ALGORITHM := SHA256_RSA4096 # 签名算法 BOARD_AVB_KEY_PATH := external/avb/test/data/testkey_rsa4096.pem # 签名密钥 # 各分区的 hashtree 配置 BOARD_AVB_SYSTEM_ADD_HASHTREE_FOOTER_ARGS += --hash_algorithm sha256 BOARD_AVB_VENDOR_ADD_HASHTREE_FOOTER_ARGS += --hash_algorithm sha256 BOARD_AVB_PRODUCT_ADD_HASHTREE_FOOTER_ARGS += --hash_algorithm sha256 # vbmeta_system 包含 system 和 system_ext 的验证信息 BOARD_AVB_VBMETA_SYSTEM := system system_ext system_dlkm BOARD_AVB_VBMETA_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem BOARD_AVB_VBMETA_SYSTEM_ALGORITHM := SHA256_RSA2048 avbtool：Google 提供的 AVB2 工具，位于 external/avb/avbtool.py。它在构建时被调用为每个分区添加 hash footer：\n# device/amlogic/common/build_kernel_modules.mk ifeq ($(BOARD_AVB_ENABLE),true) $(AVBTOOL) add_hash_footer --partition_name boot ... endif U-Boot 中的 AVB 验证：在 bootloader/uboot-repo/bl33/v2019/lib/libavb/ 目录中有完整 AVB 验证库实现：\nlib/libavb/ ├── avb_slot_verify.c # 核心验证逻辑 ├── avb_chain_partition_descriptor.c # 链式分区验证 ├── avb_hashtree_descriptor.c # hashtree 验证 ├── avb_hash_descriptor.c # hash 验证 └── avb_footer.c # footer 解析 cmd/amlogic/cmd_bootctl_avb.c 实现了 Amlogic 定制的启动控制命令，支持 AVB 模式下的 A/B 槽选择。\n内核 cmdline 传递验证状态：\n# BoardConfig.mk BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive # AVB 启用时内核会收到 androidboot.verifiedbootstate=green 等参数 5.4 实际价值 AVB2 解决了 Android 设备最根本的安全问题——系统完整性。没有 AVB2：\n攻击者可以刷入篡改过的 system.img，注入后门 OTA 升级包被替换后无法被检测 设备锁定状态无实际意义 在 Android TV 设备上，AVB2 还满足 Google CTS（兼容性测试套件）的 Verified Boot 要求，未经 AVB2 认证的设备无法预装 Google 服务。\n6. VAB (Verified Android Boot) 6.1 概念解释 概念解释：VAB（Verified Android Boot）是 Amlogic 在 AVB2 基础上增加的增强层，全称是 Verified Android Boot with A/B slot control。它不是替代 AVB2，而是在 AVB2 之上增加 A/B 系统更新的启动控制逻辑。\nVAB = AVB2 + A/B 系统更新 + 启动控制\nA/B 系统更新（无缝更新）：设备维护两个系统槽位（slot A 和 slot B），当前在用的槽位称为 active slot。OTA 更新在后台槽位进行，更新完成后重启切换到新槽位。如果新系统启动失败，自动回滚到旧槽位——用户几乎感觉不到更新过程。\n6.2 VAB 的核心职责 从 cmd_bootctl_avb.c 中的枚举可以看出启动模式：\nenum BOOT_MODE { BOOT_MODE_NORMAL = 1, // 普通启动，无验证 BOOT_MODE_AVB = 2, // AVB2 验证启动 BOOT_MODE_VAB = 3, // AVB2 + A/B 启动控制 }; VAB 模式在 AVB 验证的基础上增加了：\n读取 misc 分区的 A/B 元数据：确定当前应启动哪个槽位 槽位有效性检查：slot_is_bootable_VAB() 检查指定槽位的 metadata 是否有效 自动回滚：如果当前槽位启动失败（如 kernel panic），标记该槽位为 unbootable，切换到另一个槽位 AVB 验证 + 槽位选择的组合：先验证 vbmeta，再用 VAB 选择槽位 6.3 代码体现 构建时启用 VAB：\n./mk s7d_bm201 --avb2 --vab 在 fip/mk_script.sh 中：\n# 解析 --vab 参数 --vab) CONFIG_CMD_BOOTCTOL_VAB=1 export CONFIG_CMD_BOOTCTOL_VAB=1 ;; # 传递给 U-Boot 编译 build_uboot ${CONFIG_SYSTEM_AS_ROOT} ${CONFIG_AVB2} ${CONFIG_CMD_BOOTCTOL_VAB} \\ ${CONFIG_FASTBOOT_WRITING_CMD} ${CONFIG_AVB2_RECOVERY} ${CONFIG_TESTKEY} \\ ${CONFIG_AB_UPDATE} ${CONFIG_AML_GPT} 传入 make 的是 BOOTCTRLMODE=$3 参数，在 U-Boot 的 Makefile 中转换为 CONFIG 宏。\nmisc 分区布局（来自 cmd_bootctl_avb.c 的注释）：\nmisc 分区内存布局: 0 - 2K Bootloader Message（命令、状态、恢复信息） 2K - 16K Bootloader 私有空间（含 A/B 元数据: 2K-4K） 16K - 32K Wipe package（恢复模式清数据用） 32K - 64K 系统空间（AOSP 功能） VAB 回滚逻辑（cmd_bootctl_avb.c）：\n// 加载并验证 A/B 槽位信息 boot_info_load_VAB(\u0026amp;boot_ctrl, miscbuf); if (!boot_info_validate_VAB(\u0026amp;boot_ctrl)) { // 元数据损坏，标记为需要修复 mark_boot_info_as_unrecoverable(\u0026amp;boot_ctrl); } // 获取当前活动槽位 slot = get_active_slot_VAB(\u0026amp;boot_ctrl); // 检查槽位是否可启动 bootable_a = slot_is_bootable_VAB(\u0026amp;boot_ctrl.slot_info[0]); bootable_b = slot_is_bootable_VAB(\u0026amp;boot_ctrl.slot_info[1]); 6.4 VAB vs AVB2 对比 特性 AVB2 VAB (Amlogic) 分区完整性验证 ✅ ✅（继承 AVB2） 设备锁定/解锁 ✅ ✅ 回滚保护 ✅ ✅ A/B 槽位切换 ❌ ✅ 自动回滚 ❌ ✅ misc 分区管理 ❌ ✅ 6.5 实际价值 VAB 让 Android TV 设备的系统更新更加可靠。如果没有 VAB：\nOTA 更新过程中断电 → 设备变砖 新系统启动失败 → 需要用户手动进入 Recovery 模式恢复 无法实现\u0026quot;静默更新、重启生效\u0026quot;的无缝体验 A/B 系统（由 VAB 控制）是 Android TV 设备满足质量要求的关键特性。\n7. Fastboot 模式 概念解释：Fastboot 是 Android 设备的刷机协议，允许用户通过 USB 连接 PC，执行烧录分区、解锁设备、重启等操作。fastboot 运行在 bootloader 阶段（U-Boot 中），不依赖 Android 系统。\n代码体现：U-Boot 编译时通过 --fastboot-write 参数启用 fastboot 命令支持：\n# build_uboot 参数 build_uboot ${CONFIG_SYSTEM_AS_ROOT} ${CONFIG_AVB2} ${CONFIG_CMD_BOOTCTOL_VAB} \\ ${CONFIG_FASTBOOT_WRITING_CMD} ... 实际流程：\nPC: fastboot flash boot boot.img ↓ USB U-Boot fastboot: 接收镜像数据 ↓ U-Boot: 写入 boot 分区 ↓ U-Boot: AVB2 验证镜像签名（可选） 实际价值：Fastboot 是 Android 开发调试的核心工具。没有它，烧录系统需要拆机使用串口/USB 烧录工具，开发效率极低。量产时也可以通过 fastboot OEM 命令锁定设备。\n8. Amlogic 签名和打包体系 8.1 签名流程 根据 fip/s7d/build.sh（读取完整文件后的理解），S905X5M 的签名流程分为两层：\n芯片级签名（CS，Chipset Signing）：\n使用 RSA 算法对 BL2/BL2E/BL2X 签名 签名后生成 bb1st.bin.rsa.rsa 等文件 由 Boot ROM 使用芯片公钥验证 设备级签名（DV，Device Signing）：\n使用设备特定密钥对 FIP 头部签名 生成 device-fip-header.bin.rsa.rsa 由 BL2 验证 8.2 构建产物 build/u-boot.bin ← 原始 U-Boot 镜像 build/u-boot.bin.sd.bin ← SD 卡启动镜像 build/u-boot.bin.sd.bin.signed ← SD 卡启动签名镜像 build/u-boot.bin.usb.signed ← USB 烧录签名镜像 build/boot.img.encrypt ← 加密 boot 镜像（可选） 这些产物被复制到 device/amlogic/ross/upgrade/ 目录，作为最终固件打包的输入。\n9. 与 Android 构建系统的集成 Bootloader 的构建虽然独立于 AOSP 的 Soong/Makefile 构建系统，但通过以下方式集成：\n9.1 独立构建命令 # 在 uboot-repo 目录中单独构建 cd bootloader/uboot-repo ./mk s7d_bm201 --update-bl2 --update-bl2e --update-bl2x --update-bl31 --update-bl32 --avb2 --vab 9.2 集成到完整构建 ./mk ross -v common14-5.15 当 TARGET_NO_BOOTLOADER := false（BoardConfig.mk 第 43 行）时，构建系统会自动调用 uboot-repo 的构建流程。\n9.3 固件打包 构建完成后，bootloader 被包含在最终烧录包 aml_upgrade_package.img 中。打包配置参考 device/amlogic/common/factory.mk。\n10. 流程图汇总 Amlogic S905X5M 完整启动时序 上电 │ ▼ Boot ROM (芯片固化) │ 1. 加载 BL2 到 SRAM │ 2. 验证 BL2 签名 (芯片公钥) │ 3. 跳转到 BL2 │ ▼ BL2 (SRAM 中运行) │ 1. 初始化 DDR 内存 │ 2. 从 FIP 包中加载 BL30/BL31/BL32/BL33 到 DDR │ 3. 验证各阶段签名 │ 4. 跳转到 BL31 │ ▼ BL31 (ATF, Secure World) │ 1. 初始化 SMC 处理 │ 2. 启动 BL32 (TEE, 可选) │ 3. 跳转到 BL33 (切换到 Normal World) │ ▼ BL33 (U-Boot, Normal World) │ 1. 初始化存储、显示、网络等外设 │ 2. [AVB2 验证] 读取 vbmeta → 验证 boot.img │ 3. [VAB 控制] 选择 A/B 槽位 │ 4. 将 kernel + dtb + ramdisk 加载到内存 │ 5. 跳转到 Kernel │ ▼ Linux Kernel │ 1. 解压并初始化 │ 2. 挂载 rootfs (ramdisk) │ 3. 启动 init 进程 │ ▼ init → Android 系统 AVB2 验证链扩展 ┌────────────────────────────────────────────────────────────────┐ │ Boot ROM │ │ 熔断的公钥 hash → 验证 BL2 签名 │ └────────────────────┬───────────────────────────────────────────┘ │ 信任传递 ▼ ┌────────────────────────────────────────────────────────────────┐ │ BL2 → BL31 → BL33 (U-Boot) │ │ 每级固件都验证下一级的签名，确保启动链未被篡改 │ └────────────────────┬───────────────────────────────────────────┘ │ 信任传递 ▼ ┌────────────────────────────────────────────────────────────────┐ │ U-Boot 执行 AVB2 验证 │ │ │ │ vbmeta (用 OEM 公钥签名) │ │ ├── boot.img hash ← 使用 vbmeta 中记录的 hash 值 │ │ ├── system.img hashtree │ │ ├── vendor.img hashtree │ │ └── product.img hashtree │ │ │ │ vbmeta_system (用 system 密钥签名) │ │ ├── system_ext.img hashtree │ │ └── system_dlkm.img hashtree │ └────────────────────────────────────────────────────────────────┘ 11. 关键代码文件索引 功能 路径 U-Boot 构建入口 bootloader/uboot-repo/fip/mk_script.sh U-Boot 源码 (v2019) bootloader/uboot-repo/bl33/v2019/ BL2 源码 bootloader/uboot-repo/bl2/ BL31 (ATF v1.3) bootloader/uboot-repo/bl31/bl31_1.3/ BL31 (ATF v2.7) bootloader/uboot-repo/bl31/bl31_2.7/ S7D SoC 构建脚本 bootloader/uboot-repo/fip/s7d/build.sh AVB 验证库 bootloader/uboot-repo/bl33/v2019/lib/libavb/ AVB U-Boot 命令 bootloader/uboot-repo/bl33/v2019/cmd/avb.c VAB 启动控制 bootloader/uboot-repo/bl33/v2019/cmd/amlogic/cmd_bootctl_avb.c AVB 配置 (BoardConfig) device/amlogic/ross/BoardConfig.mk avbtool external/avb/avbtool.py 签名测试密钥 external/avb/test/data/ 设备升级配置 device/amlogic/ross/upgrade/platform.conf 12. 自问自答 Q: U-Boot 和 Boot ROM 什么关系？ A: Boot ROM 是芯片出厂固化的代码，不可修改。它负责初始化基本硬件（如 SRAM）并加载 BL2。U-Boot（BL33）在启动链的末端，是最后一个 bootloader 阶段，负责加载操作系统。Boot ROM 只信任 BL2，BL2 信任 BL31/BL33，形成信任链。\nQ: AVB2 能防什么攻击？ A: 防\u0026quot;离线篡改\u0026quot;——攻击者拆机读取 eMMC，修改 system.img 加入后门，再写回去。AVB2 的 hashtree 验证会发现根 hash 不匹配，拒绝启动。但 AVB2 不防\u0026quot;在线攻击\u0026quot;（设备运行时通过漏洞注入），那是 SELinux 和沙箱的职责。\nQ: VAB 和 AVB2 是不是二选一？ A: 不是。VAB 依赖 AVB2。AVB2 负责\u0026quot;验证镜像是否被篡改\u0026quot;，VAB 负责\u0026quot;在 AVB2 验证通过的基础上，决定启动哪个槽位\u0026quot;。可以只有 AVB2 没有 VAB，但不能只有 VAB 没有 AVB2。\nQ: A/B 系统怎么做到无缝更新？ A: 假设设备运行在 slot A。OTA 下载新系统到 slot B，安装完成后重启。U-Boot 检查到 slot B 标记为\u0026quot;更新完成\u0026quot;，切换 active slot 到 B，启动新系统。如果 B 无法启动，VAB 标记 B 为失败，自动回滚到 A。整个过程除了重启一刻，用户不需要等待安装。\nQ: Fastboot flash 为什么设备要解锁？ A: 解锁设备后，U-Boot 在验证 vbmeta 时发现设备是 unlocked 状态，会允许写入并启动未签名的镜像。锁定状态下，U-Boot 拒绝写入受保护分区（如 boot、system），也拒绝启动签名校验失败的镜像。解锁动作通常需要用户确认并清除所有数据。\nQ: 开发时可以用 testkey 吗？ A: 可以。external/avb/test/data/testkey_rsa4096.pem 是 AVB2 的测试密钥。但测试密钥公开在 AOSP 源码中，任何人都可以用它签名镜像。因此使用测试密钥的设备只能用于开发调试，不能出厂。量产时必须使用厂商私有的签名密钥。\n13. 下一步 理解 bootloader 后，建议继续深入：\nKernel 构建与设备树（notes/08-kernel-dts.md）：理解内核如何通过设备树匹配硬件 HAL 与 Treble（notes/09-hal-treble-hidl.md）：理解内核之上 Android 如何抽象硬件 SELinux（notes/10-selinux-android.md）：理解设备启动后的安全策略实施 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/07-bootloader-uboot-%E5%AE%89%E5%85%A8%E5%90%AF%E5%8A%A8/","summary":"\u003ch1 id=\"31-bootloaderu-boot--安全启动\"\u003e3.1 Bootloader：U-Boot + 安全启动\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e学习日期: 2026-04-30\n参考项目: Amlogic S905X5M (ross) Android 14 TV\n前置知识: 嵌入式系统启动流程、ARM TrustZone 基本概念\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-引言bootloader-是什么\"\u003e1. 引言：Bootloader 是什么？\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e概念解释\u003c/strong\u003e：Bootloader（启动加载器）是设备上电后第一个执行的软件。它的职责是初始化硬件（时钟、DDR 内存、存储控制器）、从存储介质（eMMC/NAND）加载操作系统映像到内存，然后跳转执行。在 Android 嵌入式系统中，bootloader 不仅要\u0026quot;启动系统\u0026quot;，还要\u003cstrong\u003e验证启动链的完整性\u003c/strong\u003e，防止被篡改的系统运行。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代码体现\u003c/strong\u003e：参考代码库中，bootloader 源码位于 \u003ccode\u003ebootloader/uboot-repo/\u003c/code\u003e，构建命令为：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e./mk s7d_bm201 --avb2 --vab\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 或针对 ross 设备：\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecd bootloader/uboot-repo \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e ./mk s7d_bm201 --update-bl2 --update-bl2e --update-bl2x --update-bl31 --update-bl32 --avb2 --vab\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e实际价值\u003c/strong\u003e：没有 bootloader，芯片上电后 CPU 不知道从哪里取指令、内存没有初始化、没有任何代码可以执行。在 Android 安全模型中，bootloader 是第一道防线——如果 bootloader 不可信，后续整个系统都不可信。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-arm-处理器启动流程基础\"\u003e2. ARM 处理器启动流程基础\u003c/h2\u003e\n\u003cp\u003e在深入 Amlogic U-Boot 之前，需要理解 ARM 处理器的启动模式：\u003c/p\u003e\n\u003ch3 id=\"21-arm-trustzone\"\u003e2.1 ARM TrustZone\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e概念解释\u003c/strong\u003e：TrustZone 是 ARM 架构的硬件安全扩展，将 SoC 内部资源划分为两个世界：\u003c/p\u003e","title":"07-3.1Bootloader：U-Boot+安全启动"},{"content":"device/amlogic/ross/ 设备配置 分析 ross (S905X5M) 设备的完整构建配置，理解一个 Android 嵌入式设备是如何定义的 参考代码库：device/amlogic/ross/，Android 14 + Amlogic S905X5M ATV\n1. 总览 device/amlogic/ross/ 是 Android 构建系统中设备配置目录。它定义了：\n选择什么 SoC（S905X5M）和平台（s7d） 选择什么 kernel（common14-5.15，GKI 方式） 选择什么 bootloader（U-Boot，支持 AVB2 + VAB） 分区布局 和文件系统类型 硬件特性（显示、音频、编解码、WiFi/BT、摄像头等） 打包哪些模块（继承 vendor/giec/ 的定制） 文件布局 device/amlogic/ross/ ├── BoardConfig.mk # ★ 板级配置（硬件、分区、AVB） ├── device.mk # ★ 设备配置（媒体、音频、GPU、显示） ├── ross.mk # ★ 产品配置主入口 ├── AndroidProducts.mk # 注册所有产品变体（lunch 选项） ├── AndroidBoard.mk # 板级构建（kernel、factory） ├── Android.mk / Android.bp │ ├── ross_atv.mk # ATV 变体 ├── ross_dongle.mk # 加密狗/电视棒变体 ├── ross_soundbar.mk # 条形音箱变体 ├── ross_hybrid.mk # Hybrid 变体 ├── ross_hybrid_atf.mk # Hybrid ATF 变体 ├── ross_cbs.mk # CBS 广播变体 ├── ross_mxl258c.mk # MXL258C 调谐器变体 │ ├── part_table_5_15.txt # ★ 分区表（AB 系统，动态分区） ├── part_table_U_8G_emmc.txt # 8G eMMC 备选分区表 ├── fstab.ab_oem.amlogic # ★ 文件系统挂载表 │ ├── vendor_prop.mk # 供应商属性 ├── product_property.mk # 产品属性（oem key） ├── kernel_config_build.mk # Kernel 构建配置 ├── wifibt.build.config.trunk.mk # WiFi/BT 模块选择 ├── AVSync.mk # 音视频同步配置 │ ├── init.amlogic.board.rc # 板级 init 脚本 ├── init.amlogic.board.ext.rc # 板级扩展 init 脚本 ├── ueventd.parallel.rc # ueventd 并行恢复上下文 ├── board-info.txt # bootloader 版本约束 │ ├── files/ # ★ 硬件配置文件（编解码、音频、显示） │ ├── media_codecs*.xml # 媒体编解码配置 │ ├── mixer_paths.xml # 音频通路配置 │ ├── mesondisplay.cfg # 显示配置 │ ├── mali_platform.config # GPU/Mali 配置 │ ├── audio_effects.conf # 音效配置 │ └── speaker_*.dat/xml # 音箱调优数据 ├── permissions/ # 权限配置文件 ├── overlay/ # 资源覆盖 ├── oem/ # OEM 分区预置内容 ├── prebuilt/ # 预编译二进制 ├── recovery/ # Recovery 相关配置 ├── logo_img_files/ # 开机 logo ├── upgrade/ # 升级相关 └── tools/ # 签名工具 2. Android 构建系统的设备配置层级 理解 Android 构建系统的层次结构 是嵌入式开发的基础。一个产品配置（ross.mk）通过 $(call inherit-product, ...) 逐层继承，最终形成完整的构建配置。\nross.mk (产品级 —— 顶层入口，定义 PRODUCT_NAME/DEVICE/BRAND/MODEL) │ ├── product_mbox.mk (Amlogic 通用产品配置，包含 TV HAL 等) │ └── core_amlogic.mk (Amlogic 核心配置) │ ├── s7d.mk (芯片平台级 —— s7d/S905X5 平台通用配置) │ ├── device.mk (板级 —— 本设备特有配置) │ └── device/amlogic/common/products/mbox/s7d/device.mk │ (s7d 平台默认板级配置，包含 keylayout、init.rc、媒体配置) │ ├── vendor_prop.mk (供应商属性) ├── product_property.mk (产品属性) │ ├── vendor/amlogic/ross/device-vendor.mk (如果存在，OEM 额外配置) │ └── device/giec/device-giec.mk (通过 device.mk 中的 inherit) 实践意义：当需要在某款设备上添加功能时，搞清楚改哪个层级的文件：\n改整个 SoC 平台 → s7d.mk 改本设备特有 → device.mk 或 ross.mk 改厂商定制 → vendor/giec/device-giec.mk 3. BoardConfig.mk —— 板级硬件定义 BoardConfig.mk 定义的是板级硬件参数：CPU 架构、分区大小、文件系统类型、AVB 配置等。\n3.1 SoC 与 CPU 架构 PRODUCT_DIR := ross TARGET_BOARD_PLATFORM := s7d # Amlogic s7d 平台 TARGET_BOOTLOADER_BOARD_NAME := ross # 64 位主架构 + 32 位兼容 TARGET_ARCH := arm64 TARGET_ARCH_VARIANT := armv8-2a TARGET_CPU_VARIANT := cortex-a55 TARGET_CPU_ABI := arm64-v8a # 32 位次架构（兼容旧应用） TARGET_2ND_ARCH := arm TARGET_2ND_ARCH_VARIANT := armv8-2a TARGET_2ND_CPU_VARIANT := cortex-a55 TARGET_SUPPORTS_32_BIT_APPS := true TARGET_SUPPORTS_64_BIT_APPS := true Cortex-A55 是 ARM v8.2-A 的中核，常用于电视盒子和嵌入式设备。支持 64 位主架构同时兼容 32 位应用。\n3.2 显示与 GPU # 显示输出 HWC_PRIMARY_FRAMEBUFFER_WIDTH := 1920 HWC_PRIMARY_FRAMEBUFFER_HEIGHT := 1080 HWC_PRIMARY_CONNECTOR_TYPE := hdmi HWC_DISPLAY_NUM := 1 # 单显示输出 HWC_ENABLE_REAL_MODE := true # 支持真实 HDMI 分辨率 HWC_ENABLE_SEAMLESS_MODE_SWITCH := true # 无缝切换分辨率 HWC_ENABLE_PRIMARY_HOTPLUG := true # 支持 HDMI 热插拔 HWC_HDMI_FRAC_MODE := 2 # HDMI 分数刷新率 # Mali GPU 配置 include hardware/amlogic/gralloc/gralloc.device.mk include hardware/amlogic/hwcomposer/hwcomposer.device.mk 3.3 摄像头 BOARD_HAVE_FRONT_CAM := false BOARD_HAVE_BACK_CAM := false BOARD_USE_USB_CAMERA := true # 仅支持 USB 摄像头 3.4 分区与文件系统 分区配置 是嵌入式 Android 中最关键的硬件参数之一。分区大小决定系统能装多少东西，文件系统类型影响性能和可靠性。\n# 文件系统类型（EROFS 是 Android 14 主推的只读文件系统） BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE := erofs BOARD_USERDATAIMAGE_FILE_SYSTEM_TYPE := f2fs # 数据分区用 F2FS BOARD_EROFS_COMPRESSOR := eamfc # EROFS 压缩算法 # 分区大小（预置在动态分区之前） BOARD_BOOTIMAGE_PARTITION_SIZE := 67108864 # 64M boot BOARD_VENDOR_BOOTIMAGE_PARTITION_SIZE := 67108864 # 64M vendor_boot BOARD_DTBOIMG_PARTITION_SIZE := 2097152 # 2M dtbo BOARD_DTBIMAGE_PARTITION_SIZE := 258048 # 252K dtb # 动态分区（super 分区） BOARD_SUPER_PARTITION_SIZE := 3355443200 # 3.2G super BOARD_AMLOGIC_DYNAMIC_PARTITIONS_SIZE := 3344957440 BOARD_AMLOGIC_DYNAMIC_PARTITIONS_PARTITION_LIST := \\ system vendor product odm system_ext \\ vendor_dlkm system_dlkm odm_dlkm 3.5 AVB（Android Verified Boot） # 每个分区独立签名 BOARD_AVB_SYSTEM_ADD_HASHTREE_FOOTER_ARGS += --hash_algorithm sha256 BOARD_AVB_VENDOR_ADD_HASHTREE_FOOTER_ARGS += --hash_algorithm sha256 BOARD_AVB_PRODUCT_ADD_HASHTREE_FOOTER_ARGS += --hash_algorithm sha256 # vbmeta_system 链式验证 BOARD_USES_VBMETA_SYSTEM := true BOARD_AVB_VBMETA_SYSTEM := system system_ext system_dlkm BOARD_AVB_VBMETA_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem BOARD_AVB_VBMETA_SYSTEM_ALGORITHM := SHA256_RSA2048 3.6 Boot 相关 BOARD_BOOT_HEADER_VERSION := 4 # GKI 要求的 boot header v4 BOARD_KERNEL_CMDLINE += bootconfig # 使用 bootconfig BOARD_BOOTCONFIG += androidboot.dynamic_partitions=true BOARD_RAMDISK_USE_LZ4 := true # LZ4 压缩 ramdisk（更快解压） BOARD_INCLUDE_DTB_IN_BOOTIMG := true # DTB 内嵌在 boot.img 中 4. 分区布局详解 分区布局 定义了 eMMC/NAND Flash 上每个分区的起始位置、大小和属性。AB 系统（A/B slot）架构下，关键分区都有 _a 和 _b 两份，确保升级失败可回滚。\n4.1 分区表文件 part_table_5_15.txt —— 适用于 5.15 kernel 的标准分区表：\n// 格式：分区名, 起始地址, 大小, 间隔, 属性 // 属性 0x0001 = 普通分区 // 属性 0x0002 = 普通分区（特殊） // 属性 0x0011 = 工厂分区（持久化） // 属性 0x1001 = 元数据分区 // 属性 0x1004 = 用户数据分区 reserved, 36M, 64M, 8M, 0 # 保留区（给 bootloader 用） env, -, 8M, 16M, 0 # U-Boot 环境变量 frp, -, 2M, 8M, 0x0001 # Factory Reset Protection factory, -, 8M, 1M, 0x0011 # 工厂数据（持久化，跨升级保留） vendor_boot_a, -, 64M, 1M, 0x0001 # vendor_boot（GKI 必须） vendor_boot_b, -, 64M, 1M, 0x0001 bootloader_a, -, 8M, 1M, 0x0001 # U-Boot bootloader_b, -, 8M, 1M, 0x0001 tee, -, 32M, 1M, 0x0001 # TrustZone 安全环境 logo, -, 8M, 1M, 0x0001 # 开机 logo misc, -, 2M, 1M, 0x0001 # misc（bootloader 通信） dtbo_a, -, 2M, 1M, 0x0001 # Device Tree Blob Overlay dtbo_b, -, 2M, 1M, 0x0001 cri_data, -, 8M, 1M, 0x0002 # 关键数据 param, -, 16M, 1M, 0x0002 # 参数分区 odm_ext_a, -, 16M, 1M, 0x0001 # ODM 扩展 odm_ext_b, -, 16M, 1M, 0x0001 oem_a, -, 32M, 1M, 0x0001 # OEM 分区（DRM 证书等） oem_b, -, 32M, 1M, 0x0001 boot_a, -, 64M, 1M, 0x0001 # boot.img（GKI kernel + ramdisk） boot_b, -, 64M, 1M, 0x0001 init_boot_a, -, 8M, 1M, 0x0001 # init_boot（GKI 通用 ramdisk） init_boot_b, -, 8M, 1M, 0x0001 metadata, -, 64M, 1M, 0x1001 # 加密元数据 vbmeta_a, -, 2M, 1M, 0x0001 # 启动验证描述符 vbmeta_b, -, 2M, 1M, 0x0001 vbmeta_system_a,-, 2M, 1M, 0x0001 # system 分区验证 vbmeta_system_b,-, 2M, 1M, 0x0001 super, -, 3200M,1M, 0x0001 # ★ 动态分区（包含 system/vendor 等） rsv, -, 64M, 1M, 0x0001 # 保留区 userdata, -, -, 1M, 0x1004 # ★ 用户数据（剩余全部空间） 4.2 分区大小汇总 分区 大小 用途 super 3200 MB (3.2 GB) 动态分区容器：system + vendor + product + odm + system_ext + vendor_dlkm + system_dlkm + odm_dlkm userdata 剩余全部 应用数据、用户文件（F2FS） vendor_boot 64 MB × 2 GKI vendor ramdisk boot 64 MB × 2 GKI kernel tee 32 MB TrustZone OS metadata 64 MB FBE 加密密钥 oem 32 MB × 2 DRM 证书、许可证 rsv 64 MB 保留 4.3 AB 系统 (Seamless OTA) 除 reserved、env、frp、factory、cri_data、param、metadata、rsv、userdata 外，所有分区都有 _a 和 _b 两份。这是 Android AB 升级的基础：\n当前在 slot A 运行，OTA 写入 slot B 重启后从 slot B 启动，如果 B 失败则自动回滚到 A 用户无感知升级 4.4 8G eMMC 版本 part_table_U_8G_emmc.txt 是针对 8GB eMMC 的备选分区表，主要区别：\nsuper 分区从 3200MB 缩小到 2400MB rsv 从 64MB 缩小到 32MB 5. fstab —— 文件系统挂载表 fstab 定义了系统启动时每个分区的挂载点、文件系统类型和挂载参数。这是内核启动后 init 进程读取的第一个配置文件之一。\n# fstab 被复制到两个位置： PRODUCT_COPY_FILES += \\ device/amlogic/ross/fstab.ab_oem.amlogic:$(TARGET_COPY_OUT_VENDOR)/etc/fstab.amlogic \\ device/amlogic/ross/fstab.ab_oem.amlogic:$(TARGET_COPY_OUT_VENDOR_RAMDISK)/first_stage_ramdisk/fstab.amlogic # 第一份 → vendor/etc/（正常运行用） # 第二份 → vendor ramdisk（早期 init 挂载用） 关键挂载项 源分区 挂载点 文件系统 关键标志 system /system erofs ro, slotselect, avb=vbmeta_system, logical vendor /vendor erofs ro, slotselect, avb=vbmeta, logical product /product erofs ro, slotselect, avb, logical system_ext /system_ext erofs ro, slotselect, avb=vbmeta_system, logical /dev/block/by-name/userdata /data f2fs latemount, fileencryption, keydirectory /dev/block/by-name/metadata /metadata f2fs wait,check,formattable /dev/block/zram0 swap swap zramsize=33% 关键观察：\n所有系统分区都用 EROFS（只读、压缩），Android 14 主推 data 用 F2FS（Flash-Friendly File System），Android 的标准数据分区文件系统 data 加密：fileencryption=aes-256-xts:aes-256-cts:v2+inlinecrypt_optimized ZRAM：33% 的 RAM 作为压缩交换空间 AVB 验证：每个系统分区都有 avb 标志 6. 变体产品 —— 同一芯片，不同形态 ross 有 8 个产品变体，体现出 Amlogic 平台的高度复用。\n变体 PRODUCT_NAME 特点 ross ross 基础 ATV (Amati Launcher) ross_atv ross_atv ATV (Waston Launcher) ross_dongle ross_dongle 电视棒/加密狗 (HDMI-only) ross_soundbar ross_soundbar 条形音箱形态 ross_hybrid ross_hybrid Hybrid 模式 ross_hybrid_atf ross_hybrid_atf Hybrid + ATF ross_cbs ross_cbs CBS 广播接收 ross_mxl258c ross_mxl258c MXL258C 调谐器 变体之间的差异很小，通常在 ross.mk 基础上覆盖少量配置：\n# ross_dongle.mk —— 只改了连接器类型和 Launcher $(call inherit-product, device/amlogic/ross/ross.mk) PRODUCT_NAME := ross_dongle HWC_PRIMARY_CONNECTOR_TYPE := hdmi-only ATV_LAUNCHER := amati 构建命令示例 # 基础 ATV 版本 source build/envsetup.sh \u0026amp;\u0026amp; lunch ross-userdebug \u0026amp;\u0026amp; ./mk ross -v common14-5.15 # ATV 版本（Waston Launcher） lunch ross_atv-userdebug \u0026amp;\u0026amp; ./mk ross_atv -v common14-5.15 # 条形音箱版 lunch ross_soundbar-userdebug \u0026amp;\u0026amp; ./mk ross_soundbar -v common14-5.15 7. Kernel 与 Bootloader 选择 7.1 Kernel # ross.mk TARGET_BUILD_KERNEL_VERSION ?= 5.15 TARGET_BUILD_KERNEL_USING_14_5.15 ?= true # Android 14 + kernel 5.15 # BoardConfig.mk BOARD_BOOT_HEADER_VERSION := 4 # GKI boot header # kernel_config_build.mk KERNEL_ROOTDIR := common # GKI 通用内核目录 include device/amlogic/common/build_kernel_modules.mk # ross.mk —— 使用预编译 GKI boot.img BOARD_PREBUILT_BOOTIMAGE := device/amlogic/ross-kernel/5.15/gki/boot-lz4.img TARGET_NO_KERNEL := true 关键点：ross 使用 GKI（Generic Kernel Image） 方式，即 Google 提供的通用内核。TARGET_NO_KERNEL := true 表示不从源码编译内核，而是直接使用预编译的 boot-lz4.img。内核模块（驱动）通过 vendor_dlkm 和 system_dlkm 分区加载。\n7.2 Bootloader # ross.mk 中配置 BUILD_WITH_AVB := true BOARD_AML_SECUREBOOT_SOC_TYPE := sc2 board-info.txt 显示 bootloader 版本约束：\nrequire board=ross require version-bootloader=01.01.250821.144210 构建命令：./mk s6_bl201 --vab --avb2\n7.3 构建完整镜像的流程 # 1. 编译 bootloader ./mk s6_bl201 --vab --avb2 # 2. 编译 Android + kernel ./mk ross -v common14-5.15 # 3. 生成镜像文件 # out/target/product/ross/ # ├── boot.img (GKI kernel + ramdisk) # ├── vendor_boot.img (vendor ramdisk) # ├── super.img (动态分区：system/vendor/product/...) # ├── vbmeta.img (AVB 验证描述符) # ├── dtbo.img (DT overlay) # ├── recovery.img (Recovery 模式) # └── ota_update.zip (OTA 升级包) 8. Hardware Feature Config —— 配置文件目录 device/amlogic/ross/files/ 是设备硬件配置文件的集散地，这些文件被复制到 vendor 分区，被 HAL 和 Framework 在运行时读取。\n8.1 媒体编解码 媒体配置是 Android TV 设备中最复杂的部分：\nfiles/media_codecs.xml # 主编解码配置 files/media_codecs_performance.xml # 性能配置 files/media_codecs_google_performance_video.xml # Google 视频性能 files/video/c2/media_codecs_amlogic_video.xml # Amlogic 视频解码（C2 框架） files/video/c2/media_codecs_amlogic_performance_video.xml files/encoder/media_codecs_amlogic_encoder.xml # Amlogic 视频编码 files/encoder/media_codecs_amlogic_performance_encoder.xml files/video/dolby_vision/media_codecs_amlogic_dolby_vision.xml # 杜比视界 8.2 音频 files/audio_effects.conf # 音效配置 files/mixer_paths.xml # ALSA mixer 通路配置 files/audio_policy_devices.xml # 音频设备策略 files/speaker_2.0.xml # 音箱调优（MS12 tuning） files/speaker_2.0_0_internal_speaker_48000_6.dat # 调优数据 8.3 GPU 与显示 files/mali_platform.config # Mali GPU 配置 files/mesondisplay.cfg # 显示模块配置 files/aml_afrc_allowlist.config # AFRC（自适应帧率）白名单 8.4 条件编译控制 # device.mk —— Dolby Vision ifeq ($(TARGET_BUILD_WITH_DOVI),true) PRODUCT_COPY_FILES += \\ files/video/dolby_vision/media_codecs_amlogic_dolby_vision.xml endif 9. 完整构建包含链 从 ross.mk 到最终系统的完整包含关系：\nross.mk │ ├── product_mbox.mk │ └── core_amlogic.mk │ ├── device/amlogic/common/products/mbox/s7d/device.mk │ └── 复制 keylayout、init.rc、media_profiles.xml、PQ 配置等 │ ├── device/amlogic/common/products/mbox/s7d/s7d.mk │ └── s7d 平台 NetFlix/DRM 等通用配置 │ ├── device.mk │ ├── media_codecs*.xml、mixer_paths.xml、mesondisplay.cfg ... │ ├── GPU (Mali)、audio (ALSA、Dolby)、DLGOverlay │ └── device/amlogic/common/products/mbox/s7d/device.mk │ ├── vendor_prop.mk → 蓝牙、媒体缓冲、remotectrl 属性 ├── product_property.mk → ro.oem.key1=ATV00100025 │ └── vendor/giec/device-giec.mk ← ★ 厂商定制（之前分析的） ├── SELinux 策略 ├── HIDL 服务 (hwstbcmdservice) ├── 预置 APK (OTAClient, BazeportLauncher...) ├── Shell 工具脚本 └── ADB/调试/签名配置 核心理解：device/amlogic/ross/ 定义的是\u0026quot;硬件是什么\u0026quot;，vendor/giec/ 定义的是\u0026quot;厂商加了什么\u0026quot;。两者通过 Android 构建系统的 inherit-product 机制组合。\n10. 学习要点 10.1 一个设备配置回答了哪些问题 问题 答案在哪里 用什么芯片？ BoardConfig.mk → TARGET_BOARD_PLATFORM := s7d CPU 几核多快？ TARGET_CPU_VARIANT := cortex-a55 多大存储？ 分区表 super=3200M, userdata=剩余 Android 版本？ PRODUCT_SHIPPING_API_LEVEL := 34 用哪个内核？ TARGET_BUILD_KERNEL_VERSION := 5.15 预装哪些应用？ ross.mk + device-giec.mk 中的 PRODUCT_PACKAGES 支持哪些视频格式？ files/media_codecs_*.xml 有没有 WiFi？ CONFIG_WIFI_MODULES := rtl8822cs 有没有 Google 服务？ BOARD_COMPILE_GAPPS_TV 安全启动怎么配置？ AVB 相关 BOARD_AVB_* 分区 AB 还是非 AB？ AB_OTA_UPDATER := true 10.2 理解 GKI GKI (Generic Kernel Image) 是 Android 14 的标准做法：Google 提供通用内核，芯片厂商通过 vendor 模块和 DLKM 分区加载驱动。\nross 的 GKI 体现在：\nBOARD_BOOT_HEADER_VERSION := 4 — GKI 要求的 boot header BOARD_PREBUILT_BOOTIMAGE — 使用预编译 GKI boot.img vendor_dlkm / system_dlkm 分区 — 存放内核模块 vendor_boot 分区 — 存放 vendor ramdisk 10.3 如何从零创建新设备？ 复制现有设备目录（如 device/amlogic/ross/） 修改 BoardConfig.mk：平台、CPU、分区大小 修改 device.mk：媒体配置、硬件特性 创建 产品名.mk：继承关系，定义 PRODUCT_NAME 更新 AndroidProducts.mk：注册新产品的 lunch 选项 修改分区表：eMMC 大小不同时调整 适配 fstab：分区布局不同时修改 适配硬件配置：files/ 下的编解码、音频、显示配置 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/06-%E8%AE%BE%E5%A4%87%E9%85%8D%E7%BD%AE/","summary":"\u003ch1 id=\"deviceamlogicross-设备配置\"\u003edevice/amlogic/ross/ 设备配置\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e分析 ross (S905X5M) 设备的完整构建配置，理解一个 Android 嵌入式设备是如何定义的\n参考代码库：\u003ccode\u003edevice/amlogic/ross/\u003c/code\u003e，Android 14 + Amlogic S905X5M ATV\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-总览\"\u003e1. 总览\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003edevice/amlogic/ross/\u003c/code\u003e 是 Android 构建系统中\u003cstrong\u003e设备配置目录\u003c/strong\u003e。它定义了：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e选择什么 \u003cstrong\u003eSoC\u003c/strong\u003e（S905X5M）和平台（s7d）\u003c/li\u003e\n\u003cli\u003e选择什么 \u003cstrong\u003ekernel\u003c/strong\u003e（common14-5.15，GKI 方式）\u003c/li\u003e\n\u003cli\u003e选择什么 \u003cstrong\u003ebootloader\u003c/strong\u003e（U-Boot，支持 AVB2 + VAB）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e分区布局\u003c/strong\u003e 和文件系统类型\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e硬件特性\u003c/strong\u003e（显示、音频、编解码、WiFi/BT、摄像头等）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e打包哪些模块\u003c/strong\u003e（继承 vendor/giec/ 的定制）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"文件布局\"\u003e文件布局\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edevice/amlogic/ross/\n├── BoardConfig.mk                  # ★ 板级配置（硬件、分区、AVB）\n├── device.mk                       # ★ 设备配置（媒体、音频、GPU、显示）\n├── ross.mk                         # ★ 产品配置主入口\n├── AndroidProducts.mk              # 注册所有产品变体（lunch 选项）\n├── AndroidBoard.mk                 # 板级构建（kernel、factory）\n├── Android.mk / Android.bp\n│\n├── ross_atv.mk                     # ATV 变体\n├── ross_dongle.mk                  # 加密狗/电视棒变体\n├── ross_soundbar.mk                # 条形音箱变体\n├── ross_hybrid.mk                  # Hybrid 变体\n├── ross_hybrid_atf.mk              # Hybrid ATF 变体\n├── ross_cbs.mk                     # CBS 广播变体\n├── ross_mxl258c.mk                 # MXL258C 调谐器变体\n│\n├── part_table_5_15.txt             # ★ 分区表（AB 系统，动态分区）\n├── part_table_U_8G_emmc.txt        # 8G eMMC 备选分区表\n├── fstab.ab_oem.amlogic            # ★ 文件系统挂载表\n│\n├── vendor_prop.mk                  # 供应商属性\n├── product_property.mk             # 产品属性（oem key）\n├── kernel_config_build.mk          # Kernel 构建配置\n├── wifibt.build.config.trunk.mk    # WiFi/BT 模块选择\n├── AVSync.mk                       # 音视频同步配置\n│\n├── init.amlogic.board.rc           # 板级 init 脚本\n├── init.amlogic.board.ext.rc       # 板级扩展 init 脚本\n├── ueventd.parallel.rc             # ueventd 并行恢复上下文\n├── board-info.txt                  # bootloader 版本约束\n│\n├── files/                          # ★ 硬件配置文件（编解码、音频、显示）\n│   ├── media_codecs*.xml           #   媒体编解码配置\n│   ├── mixer_paths.xml             #   音频通路配置\n│   ├── mesondisplay.cfg            #   显示配置\n│   ├── mali_platform.config        #   GPU/Mali 配置\n│   ├── audio_effects.conf          #   音效配置\n│   └── speaker_*.dat/xml           #   音箱调优数据\n├── permissions/                    # 权限配置文件\n├── overlay/                        # 资源覆盖\n├── oem/                            # OEM 分区预置内容\n├── prebuilt/                       # 预编译二进制\n├── recovery/                       # Recovery 相关配置\n├── logo_img_files/                 # 开机 logo\n├── upgrade/                        # 升级相关\n└── tools/                          # 签名工具\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"2-android-构建系统的设备配置层级\"\u003e2. Android 构建系统的设备配置层级\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e理解 Android 构建系统的层次结构\u003c/strong\u003e 是嵌入式开发的基础。一个产品配置（\u003ccode\u003eross.mk\u003c/code\u003e）通过 \u003ccode\u003e$(call inherit-product, ...)\u003c/code\u003e 逐层继承，最终形成完整的构建配置。\u003c/p\u003e","title":"06-device/amlogic/ross/设备配置"},{"content":"预置 APK 分析 分析 vendor/giec/apps/ 下所有预置 APK 的构建方式、权限模型和功能架构 参考代码库：vendor/giec/apps/，Android 14 + Amlogic S905X5M ATV\n1. 总览 device-giec.mk 中声明的预置 APK：\nPRODUCT_PACKAGES += \\ LeanKeyboard \\ # ATV 定制键盘 OTAClient \\ # OTA 升级客户端 BazeportLauncher \\ # 定制 Launcher（桌面） BazeportSystem \\ # 系统设置/工具 luojHello # 测试程序（C 程序，非 APK） 加上条件编译的：\n# 当 NEED_GLAUNCHER=true 时 PRODUCT_PACKAGES += Glauncher # 当 NEED_FACTORY_TEST=true 时 PRODUCT_PACKAGES += STB_TEST 所有 APK 的公共特征：\n都是预编译的（prebuilt） .apk 文件，不带 Java 源码 全部使用 LOCAL_CERTIFICATE := platform —— 平台密钥签名，获得 system 级 UID 通过 Android.mk + $(BUILD_PREBUILT) 集成到构建系统 安装路径取决于 LOCAL_PRIVILEGED_MODULE 和 LOCAL_SYSTEM_EXT_MODULE 2. Android 预置 APK 的构建模式 APK 预置：Android 构建系统中，厂商可以将预编译的 .apk 文件直接打包进系统镜像，而不需要从源码编译。这是厂商定制的标准做法，尤其当 APK 是闭源或由第三方提供时。\n构建系统中有两种 APK 集成方式：\n$(BUILD_PREBUILT) — 直接使用预编译的 APK，重新签名 $(BUILD_PACKAGE) — 从 Java/Kotlin 源码编译成 APK 本参考代码库中全是 prebuilt 方式。\n2.1 简单预置模式 用于 OTAClient、LeanKeyboard、luojHello 等：\nLOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # 自动取当前目录下唯一的 .apk 文件名作为模块名 APPS := $(notdir $(wildcard $(LOCAL_PATH)/*.apk)) APP_NAME := $(basename $(APPS)) LOCAL_MODULE := $(APP_NAME) LOCAL_SRC_FILES := $(APPS) LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform # ★ 使用平台密钥签名 LOCAL_PRIVILEGED_MODULE := false include $(BUILD_PREBUILT) 2.2 复杂预置模式（带权限配置） 用于 BazeportLauncher：\nLOCAL_PRIVILEGED_MODULE := true # ★ 特权应用 LOCAL_SYSTEM_EXT_MODULE := true # ★ 装在 system_ext 分区 LOCAL_PREBUILT_JNI_LIBS := ... # ★ 携带 Native 库 LOCAL_REQUIRED_MODULES += \\ # ★ 需要额外配置文件 privapp-permissions-bazeport-next.xml \\ default-permissions-bazeport-next.xml 2.3 LOCAL_CERTIFICATE 字段含义 Android 签名机制：Android 要求每个 APK 都必须用证书签名。系统在安装 APK 时会验证签名。对于系统预置的应用，构建系统可以在打包时重新签名。\n值 含义 效果 platform 用平台的 platform 密钥 APK 获得 android.uid.system UID，可申请系统级权限 media 用 media 密钥 获得 android.uid.media UID，可访问媒体资源 PRESIGNED 保留 APK 原有签名 不修改原签名，适用于有独立签名要求的应用 3. OTAClient —— OTA 升级客户端 3.1 基本信息 条目 值 包名 com.giec.otaclient 版本 1.0.6 技术栈 Kotlin + Jetpack Compose 目标 SDK 34 (Android 14) minSdk 30 (Android 11) APK 大小 43MB DEX 数量 3 个 3.2 权限清单 权限 用途 RECEIVE_BOOT_COMPLETED 开机启动检查更新 REBOOT 升级完成后重启系统 RECOVERY 进入 Recovery 模式刷机 MOUNT_UNMOUNT_FILESYSTEMS 挂载系统分区写入升级包 MANAGE_EXTERNAL_STORAGE 读写 /sdcard/ 下载升级包 FOREGROUND_SERVICE_DATA_SYNC 前台下载服务 READ_PRIVILEGED_PHONE_STATE 获取设备标识 SYSTEM_ALERT_WINDOW 升级提醒弹窗 3.3 应用架构 从 DEX 中提取的完整包结构：\ncom.giec.otaclient/ ├── OTAClientApplication # Application 入口 ├── receiver/ │ └── BootCompleteReceiver # 开机广播 → 触发自动检查更新 ├── service/ │ └── AutoCheckWorker # 后台静默检查更新（WorkManager） ├── ui/ │ ├── activities/ │ │ ├── MainActivity # 主界面/OTA 入口 │ │ ├── OnlineCheckActivity # 在线检查更新 │ │ ├── OTADownloadActivity # 下载升级包 │ │ ├── OTAUpdateActivity # 执行系统升级 │ │ ├── FileSelectorActivity # 选择本地升级包文件 │ │ └── RebootDialogActivity # 重启确认弹窗 │ ├── views/ # Compose UI 组件 │ └── theme/ # 主题 ├── viewmodel/ │ ├── DialogViewModel # 弹窗状态管理 │ └── FileSelectorViewModel # 文件选择状态管理 ├── data/ │ └── FileItemData # 文件条目数据模型 └── util/ ├── NetworkRequestUtils # 网络请求封装 ├── OkHttpUtils # OkHttp 客户端 ├── ResponseUtils # 响应解析 ├── UpdateParserUtils # 升级包信息解析 └── Md5Utils # MD5 校验 3.4 OTA 升级流程 根据 README 和代码结构，完整 OTA 流程如下：\n开机 │ ▼ BootCompleteReceiver ← 监听 BOOT_COMPLETED 广播 │ ▼ AutoCheckWorker ← WorkManager 后台任务，静默请求 OTA 服务器 │ ├─ 无更新 → 静默结束 │ ▼ (有更新) OnlineCheckActivity ← 展示新版本信息 │ ▼ (用户确认) OTADownloadActivity ← 从服务器下载升级包 │ → /sdcard/Download/ota_gateway_net.zip ▼ OTAUpdateActivity ← 复制到系统更新目录 │ → /data/ota_package/update.zip ▼ UpdateManagerUtils ← 调用 Android update_engine / RecoverySystem │ ▼ RebootDialogActivity ← 确认重启 │ ▼ 系统重启进入 Recovery → 应用升级 3.5 OTA 服务器配置 通过系统属性配置（在 device-giec.mk 或 device.mk 中设置）：\n# ohm 设备 persist.sys.otaurl.ext=http://192.168.140.41:48080/app-api/ota/device/check vendor.build.app.id=KhFacd vendor.build.app.secret=fXP1K3DFfY2HgDoB # oppen 设备 persist.sys.otaurl.ext=http://192.168.140.41:48080/app-api/ota/device/check vendor.build.app.id=g0XDey vendor.build.app.secret=X8tWx1wg5WcXPTdM 3.6 三种升级模式 服务器下发 updatePattern 参数定义：\n模式 值 行为 立即升级 1 强制弹窗，显示下载进度条，可取消 确认升级 2 弹窗确认后下载，不可取消 静默升级 3 静默下载，用户无感知（未完成） 4. BazeportLauncher —— 定制桌面 4.1 基本信息 条目 值 包名 com.bazeport.next 版本 1.1.4 (versionCode 114) 技术栈 Flutter（基于 libflutter.so + libapp.so） 目标 SDK 33 (Android 13) 安装位置 /system_ext/priv-app/BazeportLauncher/ Flutter 的证据：lib/ 目录下包含 libflutter.so（7.6MB）和 libapp.so（14MB），这是典型的 Flutter 应用结构——libflutter.so 是 Flutter 引擎，libapp.so 是 Dart 代码编译产物。\n4.2 权限分析 —— 86 个权限 BazeportLauncher 声明了 86 个权限，是参考代码库中权限最多的应用。按功能分组：\n系统管理权限：\n权限 作用 INSTALL_PACKAGES 安装/更新 APK DELETE_PACKAGES 卸载应用 REBOOT / SHUTDOWN 重启/关机 CLEAR_APP_CACHE / CLEAR_APP_USER_DATA 清理应用数据 FORCE_STOP_PACKAGES 强制停止应用 SET_TIME / SET_TIME_ZONE 设置时间 硬件控制权限：\n权限 作用 HDMI_CEC HDMI-CEC 控制 INJECT_EVENTS 注入输入事件（模拟按键） CAPTURE_VIDEO_OUTPUT / CAPTURE_AUDIO_OUTPUT 屏幕/音频录制 HARDWARE_TEST 硬件测试 MANAGE_USB USB 设备管理 网络权限：\n权限 作用 NETWORK_STACK 网络栈管理 TETHER_PRIVILEGED 网络共享 OVERRIDE_WIFI_CONFIG 覆盖 WiFi 配置 CHANGE_CONFIGURATION 修改系统配置 4.3 三类权限配置 Android 10+ 引入了对系统特权应用（PrivApp）的权限管控机制，要求权限声明必须通过 XML 文件在白名单中明确列出，否则系统不会授予这些权限。\n第一类：privapp-permissions（特权白名单）\nprivapp-permissions-bazeport-next.xml 放在 /system_ext/etc/permissions/，包含约 60 个系统级权限。系统在开机时加载此文件，授予 com.bazeport.next 对应的权限。\n\u0026lt;privapp-permissions package=\u0026#34;com.bazeport.next\u0026#34;\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.INSTALL_PACKAGES\u0026#34;/\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.REBOOT\u0026#34;/\u0026gt; \u0026lt;!-- ... 约 60 个 --\u0026gt; \u0026lt;/privapp-permissions\u0026gt; 第二类：default-permissions（运行时权限预授权）\ndefault-permissions-bazeport-next.xml 放在 /system_ext/etc/default-permissions/，用于预授权运行时权限，避免用户使用时弹窗：\n\u0026lt;exception package=\u0026#34;com.bazeport.next\u0026#34;\u0026gt; \u0026lt;permission name=\u0026#34;android.permission.ACCESS_FINE_LOCATION\u0026#34; fixed=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;!-- fixed=\u0026#34;true\u0026#34; → 用户不可撤销此权限 --\u0026gt; \u0026lt;/exception\u0026gt; 第三类：AndroidManifest.xml 中的 uses-permission\nAPK 自身的清单文件中声明的权限，与应用请求的权限对应。\n三种配置的关系：\nApp 在 AndroidManifest.xml 声明需要什么权限 │ ▼ 如果是普通权限 → 安装时自动授予 如果是危险权限 → 运行时请求（default-permissions 可预授权） 如果是系统级权限 → 必须在 privapp-permissions XML 中白名单 同时 APK 必须用 platform 密钥签名 4.4 Native 库分析 lib/ 目录下携带了大量原生库：\n库 大小 用途 libflutter.so 7.6 MB Flutter 引擎核心 libapp.so 14.0 MB Flutter 应用代码（Dart 编译产物） librive_text.so 40.9 MB Rivet 文本渲染引擎（动画文本） libpdfium.so 3.1 MB PDF 渲染引擎 libpdfrx.so 1.4 MB PDF 读取扩展 libffmpegJNI.so 828 KB FFmpeg 多媒体解码 JNI 桥接 libexosmlext.so 166 KB ExoPlayer 扩展 libpimdec-android.so 29 KB PIM 解码 Flutter 应用通过 libapp.so 将 Dart 代码编译为原生代码，因此 APK 中没有 DEX 文件中的应用逻辑。这种方式的好处是启动速度快、性能接近原生。\n4.5 启动声明 \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.MAIN\u0026#34; /\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.HOME\u0026#34; /\u0026gt; \u0026lt;!-- 桌面 --\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.LEANBACK_LAUNCHER\u0026#34; /\u0026gt; \u0026lt;!-- ATV --\u0026gt; \u0026lt;/intent-filter\u0026gt; 同时声明了 HOME（系统桌面）和 LEANBACK_LAUNCHER（ATV 启动器），意味着它会替换 Android TV 的原生桌面。\n5. BazeportSystem —— 系统设置工具 5.1 基本信息 条目 值 包名 com.bazeport.system 版本 1.4.0 (versionCode 140) APK 大小 40MB 从 AndroidManifest 中可以看到它声明了与 BazeportLauncher 类似的大量系统权限，但少了很多（没有 INSTALL_PACKAGES、REBOOT 等），定位是辅助工具。\n5.2 安装配置 LOCAL_CERTIFICATE := platform # 没有 LOCAL_PRIVILEGED_MODULE ← 不是特权应用 # 没有 LOCAL_REQUIRED_MODULES ← 不需要权限 XML 与 BazeportLauncher 不同，BazeportSystem 没有声明为特权应用（PRIVILEGED_MODULE 默认为 false），也不需要单独的权限 XML。说明它需要的权限相对温和。\n6. Glauncher —— 可选的定制桌面 6.1 基本信息 条目 值 包名 cn.giec.glauncher 版本 1.1.5 目标 SDK 34 minSdk 21 6.2 条件编译 # device-giec.mk 中 ifeq ($(NEED_GLAUNCHER), true) PRODUCT_PACKAGES += Glauncher endif 通过环境变量控制是否编译，与 BazeportLauncher 是二选一的关系。\n6.3 与 BazeportLauncher 对比 维度 BazeportLauncher Glauncher 技术栈 Flutter 传统 Android 包名 com.bazeport.next cn.giec.glauncher APK 大小 7.6 MB 29 MB 权限数量 86 个 13 个 默认启用 是 否（需 NEED_GLAUNCHER=true） 安装位置 system_ext/priv-app system/priv-app Glauncher 权限少得多，只有基础的位置、网络、存储和 WiFi 权限，没有系统管理特权。两套 Launcher 可能是针对不同的客户需求。\n7. LeanKeyboard —— ATV 定制键盘 7.1 基本信息 条目 值 包名 com.liskovsoft.leankeyboard 版本 6.1.13 来源 GitHub: yuliskov/LeanKeyboard 签名 平台密钥（重签名） 7.2 为什么需要它？ Android TV 默认的键盘在遥控器操作下体验很差。LeanKeyboard 专为电视优化：\n大字体、大按键间距 全遥控器支持（红外/蓝牙） 无 Google 服务依赖（在国产 Android TV 上至关重要，因为多数 ATV 设备不带 Google 服务） 多语言支持 7.3 系统配置要求 它声明了 BIND_INPUT_METHOD 权限（系统级），这意味着它作为系统输入法被注册。要使其生效，还需要在 Framework 配置中启用它（通常在 vendor/overlay/ 中配置）：\n\u0026lt;!-- 系统设置中启用 LeanKeyboard 作为默认输入法 --\u0026gt; \u0026lt;string-array name=\u0026#34;config_input_methods\u0026#34;\u0026gt; \u0026lt;item\u0026gt;com.liskovsoft.leankeyboard/com.liskovsoft.leankeyboard.LeanKeyKeyboard\u0026lt;/item\u0026gt; \u0026lt;/string-array\u0026gt; 8. STB-TEST —— 产线测试工具 8.1 基本信息 条目 值 包名 com.giec.stb.test 版本 1.4.8 (versionCode 20251023) 签名 平台密钥 8.2 功能 产线测试工具，通过 U 盘加载配置文件进行自动测试：\nU 盘根目录放入 giec_config.配置说明.txt 插入待测设备 按提示操作执行各项硬件测试 全部通过后标记为\u0026quot;已测试\u0026quot; 条件编译控制：\nifeq ($(NEED_FACTORY_TEST), true) PRODUCT_PACKAGES += STB_TEST endif 9. luojHello —— C 测试程序 9.1 这不是 APK 虽然 device-giec.mk 的 PRODUCT_PACKAGES 列表中包含了 luojHello，但它是一个 C 程序，不是 APK：\n#include \u0026lt;stdio.h\u0026gt; int main(void) { printf(\u0026#34;Hello Android\\n\u0026#34;); return 0; } cc_binary { name: \u0026#34;luojHello\u0026#34;, srcs: [\u0026#34;luojHello.c\u0026#34;], cflags: [\u0026#34;-Wall\u0026#34;], vendor: true, # 安装在 vendor 分区 } 编译产物是 /vendor/bin/luojHello 可执行文件，可以在 adb shell 中直接运行。\n9.2 学习价值 这是一个绝佳的最小验证工具——修改后编译进系统，确认构建流程正常工作。适合用作：\n验证 vendor 分区挂载是否正确 测试 PRODUCT_PACKAGES 机制 确认自定义模块能成功编译和打包 10. 签名与安全总结 10.1 Android 四组签名密钥 Android 系统定义了四组标准签名密钥，每组对应不同的 UID 和权限等级。系统在安装 APK 时，根据签名密钥决定授予什么级别的权限。\n密钥文件 对应 UID 典型用途 testkey.pk8/.x509.pem 应用自有 UID 开发者调试 platform.pk8/.x509.pem android.uid.system 系统应用 shared.pk8/.x509.pem android.uid.shared 共享 UID 的应用 media.pk8/.x509.pem android.uid.media 媒体相关应用 密钥文件位于 build/make/target/product/security/（testkey）或厂商自己的安全目录。\n10.2 本参考代码库的签名策略 # device-giec.mk ifeq ($(USE_RELEASE_KEY),true) PRODUCT_DEFAULT_DEV_CERTIFICATE := $(CERTIFICATE_DIR)/releasekey else # 默认使用 AOSP 测试密钥 PRODUCT_DEFAULT_DEV_CERTIFICATE := build/make/target/product/security/testkey endif 所有预置 APK 的 LOCAL_CERTIFICATE := platform 意味着它们都会使用 PRODUCT_DEFAULT_DEV_CERTIFICATE 目录下的 platform.pk8 签名。\n10.3 安全分层 system_ext/priv-app/ ← privapp 特权应用（BazeportLauncher） ├── BazeportLauncher.apk (platform 签名) ├── etc/permissions/privapp-permissions-*.xml ← 特权白名单 └── etc/default-permissions/default-permissions-*.xml ← 权限预授权 system/priv-app/ ← 普通特权应用（Glauncher） └── Glauncher.apk (platform 签名) system/app/ ← 系统应用（OTAClient, LeanKeyboard, ...） ├── OTAClient.apk (platform 签名) ├── LeanKeyboard.apk (platform 签名) └── ... vendor/bin/ ← 原生可执行文件 └── luojHello 11. 学习要点 11.1 预置 APK 的核心问题 为什么都用 platform 签名？ 因为厂商应用需要访问系统 API（如 REBOOT、INSTALL_PACKAGES），只有 platform 签名的 APK 才能获得这些权限。\n为什么 BazeportLauncher 需要单独的权限 XML（而 OTAClient 不需要）？ 在 Android 10+ 上，只有枚举在 privapp-permissions-\u0026lt;package\u0026gt;.xml 中的系统级权限才会被授予。BazeportLauncher 的权限涉及系统核心操作（安装包、重启），而这些权限对系统安全性影响重大，Android 要求显式白名单。\n为什么 Flutter 应用需要额外的权限 xml？ 这与技术栈无关，与权限等级有关。只要应用声明了\u0026quot;系统级\u0026quot;（protectionLevel=signature|privileged）的权限，就必须有白名单。\n11.2 从代码库中学到的模式 场景 做法 需要系统级权限的应用 LOCAL_CERTIFICATE := platform + privapp-permissions XML 需要预授权运行时权限 default-permissions XML 需要 native 库的 APK LOCAL_PREBUILT_JNI_LIBS 自动扫描 修改系统桌面 声明 HOME + LEANBACK_LAUNCHER 类别 开机自启服务 BootCompleteReceiver 监听 BOOT_COMPLETED 按需编译 ifeq($(VAR),true) 条件包含 移除预置应用 remove_unused_module + LOCAL_OVERRIDES_PACKAGES 11.3 深入阅读 分析 OTA 完整流程：bootable/recovery/ + frameworks/base/core/java/android/os/RecoverySystem.java 理解权限系统：frameworks/base/services/core/java/com/android/server/pm/permission/ Flutter 应用集成 AOSP：搜索 Flutter engine 的 AOSP 构建方式 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F/05-%E9%A2%84%E7%BD%AEapk%E5%88%86%E6%9E%90/","summary":"\u003ch1 id=\"预置-apk-分析\"\u003e预置 APK 分析\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e分析 \u003ccode\u003evendor/giec/apps/\u003c/code\u003e 下所有预置 APK 的构建方式、权限模型和功能架构\n参考代码库：\u003ccode\u003evendor/giec/apps/\u003c/code\u003e，Android 14 + Amlogic S905X5M ATV\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-总览\"\u003e1. 总览\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003edevice-giec.mk\u003c/code\u003e 中声明的预置 APK：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ePRODUCT_PACKAGES \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    LeanKeyboard \u003cspan style=\"color:#ae81ff\"\u003e\\ \u003c/span\u003e      \u003cspan style=\"color:#75715e\"\u003e# ATV 定制键盘\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    OTAClient \u003cspan style=\"color:#ae81ff\"\u003e\\ \u003c/span\u003e         \u003cspan style=\"color:#75715e\"\u003e# OTA 升级客户端\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    BazeportLauncher \u003cspan style=\"color:#ae81ff\"\u003e\\ \u003c/span\u003e  \u003cspan style=\"color:#75715e\"\u003e# 定制 Launcher（桌面）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    BazeportSystem \u003cspan style=\"color:#ae81ff\"\u003e\\ \u003c/span\u003e    \u003cspan style=\"color:#75715e\"\u003e# 系统设置/工具\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    luojHello            \u003cspan style=\"color:#75715e\"\u003e# 测试程序（C 程序，非 APK）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e加上条件编译的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 当 NEED_GLAUNCHER=true 时\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ePRODUCT_PACKAGES \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e Glauncher\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 当 NEED_FACTORY_TEST=true 时\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ePRODUCT_PACKAGES \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e STB_TEST\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e所有 APK 的公共特征：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e都是\u003cstrong\u003e预编译的（prebuilt）\u003c/strong\u003e \u003ccode\u003e.apk\u003c/code\u003e 文件，不带 Java 源码\u003c/li\u003e\n\u003cli\u003e全部使用 \u003ccode\u003eLOCAL_CERTIFICATE := platform\u003c/code\u003e —— 平台密钥签名，获得 system 级 UID\u003c/li\u003e\n\u003cli\u003e通过 \u003ccode\u003eAndroid.mk\u003c/code\u003e + \u003ccode\u003e$(BUILD_PREBUILT)\u003c/code\u003e 集成到构建系统\u003c/li\u003e\n\u003cli\u003e安装路径取决于 \u003ccode\u003eLOCAL_PRIVILEGED_MODULE\u003c/code\u003e 和 \u003ccode\u003eLOCAL_SYSTEM_EXT_MODULE\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-android-预置-apk-的构建模式\"\u003e2. Android 预置 APK 的构建模式\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eAPK 预置\u003c/strong\u003e：Android 构建系统中，厂商可以将预编译的 \u003ccode\u003e.apk\u003c/code\u003e 文件直接打包进系统镜像，而不需要从源码编译。这是厂商定制的标准做法，尤其当 APK 是闭源或由第三方提供时。\u003c/p\u003e","title":"05-预置APK分析"},{"content":"Level 3：加一个 native 服务开机自启动 我想做 在系统里加一个自己的可执行文件，开机自动跑起来，logcat 能看到它的输出。\n为什么要先做这个再做驱动？因为它最快让你体验完整的\u0026quot;加代码→配构建→配 init.rc→写 SELinux policy→编译→烧录→验证\u0026quot;闭环。不到你手写驱动的时间的 1/10，但建立的操作习惯后面每个 Level 都在用。\n先回答三个问题 Android 系统启动后，用户态的入口是什么？\ninit 进程是所有用户态进程的祖先（PID 1） init 解析 init.rc 文件，按规则启动服务 厂商自己的服务可以通过 init_rc 属性添加到 vendor 分区 vendor 分区和 system 分区是什么关系？\nsystem 分区存 Android 通用组件 vendor 分区存厂商（Amlogic/你的公司）的专有部分 Treble 架构强制隔离二者——你的服务应该放 vendor 分区 为什么加了服务还要写 SELinux policy？\nAndroid 的 SELinux 默认不让任何未定义的进程运行 不加 .te 文件，你的服务一启动就被 kernel 杀掉 不是 bug，是机制 需要懂的知识 Android.bp（Soong 构建）\nAOSP 从 Android 7 开始引入 Soong 替代 Makefile，配置文件是 Android.bp。常用模块类型：\ncc_binary { // 编译为可执行文件 name: \u0026#34;luojService\u0026#34;, srcs: [\u0026#34;luojService.cpp\u0026#34;], shared_libs: [ \u0026#34;liblog\u0026#34;, ], vendor: true, // 安装到 vendor 分区 } vendor: true 决定了产物的安装路径。\n源码在 vendor/giec/apps/luojTest/luojHello.c 编译产物在 out/target/product/ross/vendor/bin/luojHello 烧录后出现在板子的 /vendor/bin/luojHello 流程：Android.bp 中的 vendor: true → Soong 将产物输出到 out/.../vendor/bin/ → 打包 vendor.img 时这个目录成为 /vendor/ 分区 → 板子上就是 /vendor/bin/luojHello。\n如果去掉 vendor: true，默认会安装到 system/bin/，但 Treble 架构下厂商模块应该放 vendor 分区。\nAndroid.mk 写法（项目中同样常见）\nLOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := luojService LOCAL_SRC_FILES := luojService.cpp LOCAL_SHARED_LIBRARIES := liblog LOCAL_INIT_RC := luojService.rc LOCAL_VENDOR_MODULE := true include $(BUILD_EXECUTABLE) 这里 LOCAL_VENDOR_MODULE := true 等同于 Android.bp 的 vendor: true。\ninit.rc 语法\ninit.rc 是 init 进程的配置文件：\nservice luojService /vendor/bin/luojService class late_start user root group root disabled on property:sys.boot_completed=1 start luojService 核心概念：\nservice ——定义如何启动一个进程 class late_start ——late_start 类服务在系统基本启动完成后拉起 on property:X=Y ——当系统属性 X 等于 Y 时触发操作 SELinux policy 最小模板\n每个新增的服务都需要三个声明：\ntype luojService, domain; type luojService_exec, exec_type, vendor_file_type, file_type; init_daemon_domain(luojService) allow 语句的格式：allow source_type target_type:class { permission };\n什么时候需要 .te 文件？ 只有当进程以独立 domain 身份后台自启动（通过 init.rc）时才需要配 SELinux policy。 如果是 adb shell 手动执行，进程继承 shell domain —— userdebug/eng 版本中 shell 域权限宽松。\nfile_contexts —— 第三步不可少 .te 文件只定义了 domain 和 exec_type 的标签名，但没告诉 SELinux \u0026ldquo;哪个文件路径对应哪个标签\u0026rdquo;。\n环节 在哪配置 作用 定义 domain 和 exec_type .te 文件 声明这两个标签存在 声明 init 允许执行 init_daemon_domain(domain) 允许 init 执行对应 exec_type 并转到新 domain 路径→标签映射 file_contexts /vendor/bin/xxx → xxx_exec 检查方法：adb shell ls -Z /vendor/bin/xxx，看标签是否为自己定义的 exec_type。\n动手方案 # 第 1 步：创建模块目录 mkdir -p vendor/giec/apps/luojService/ # 第 2 步：写源码 cat \u0026gt; vendor/giec/apps/luojService/luojService.cpp \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; #include \u0026lt;android/log.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define LOG_TAG \u0026#34;luojService\u0026#34; int main() { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, \u0026#34;luojService started\u0026#34;); int count = 0; while (1) { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, \u0026#34;heartbeat #%d\u0026#34;, count++); sleep(5); } return 0; } EOF # 第 3 步：写 Android.bp cat \u0026gt; vendor/giec/apps/luojService/Android.bp \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; cc_binary { name: \u0026#34;luojService\u0026#34;, srcs: [\u0026#34;luojService.cpp\u0026#34;], shared_libs: [ \u0026#34;liblog\u0026#34;, ], init_rc: [\u0026#34;luojService.rc\u0026#34;], vendor: true, } EOF # 第 4 步：写 init.rc cat \u0026gt; vendor/giec/apps/luojService/luojService.rc \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; service luojService /vendor/bin/luojService class late_start user root group root disabled on property:sys.boot_completed=1 start luojService EOF # 第 5 步：写 SELinux policy cat \u0026gt; vendor/giec/common/sepolicy/luojService.te \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; type luojService, domain; type luojService_exec, exec_type, vendor_file_type, file_type; init_daemon_domain(luojService) EOF # 第 6 步：加 file_contexts # 编辑 vendor/giec/common/sepolicy/file_contexts，添加： # /vendor/bin/luojService u:object_r:luojService_exec:s0 # 第 7 步：在 PRODUCT_PACKAGES 中注册 # 编辑 vendor/giec/device-giec.mk，在 PRODUCT_PACKAGES 中追加 luojService # 第 8 步：编译烧录（参考 level-2） # 第 10 步：验证 adb shell ps -A | grep luojService adb shell logcat -s luojService adb shell dmesg | grep avc | grep luojService 验收清单 项目 验证方式 服务开机自启动 adb shell ps -A | grep luojService 能看到进程 logcat 有输出 adb shell logcat -s luojService 看到 heartbeat 日志 没有 SELinux 拒绝 adb shell dmesg | grep avc | grep luojService 无输出 理解每层的作用 你能说清楚：Android.bp 做什么、init.rc 做什么、.te 做什么 SELinux 标签正确 adb shell ls -Z /vendor/bin/luojService 输出 u:object_r:luojService_exec:s0 实操中遇到的问题记录在 debug.md\n","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-3-native%E6%9C%8D%E5%8A%A1/","summary":"\u003ch1 id=\"level-3加一个-native-服务开机自启动\"\u003eLevel 3：加一个 native 服务开机自启动\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e在系统里加一个自己的可执行文件，开机自动跑起来，\u003ccode\u003elogcat\u003c/code\u003e 能看到它的输出。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为什么要先做这个再做驱动？因为它最快让你体验完整的\u0026quot;加代码→配构建→配 init.rc→写 SELinux policy→编译→烧录→验证\u0026quot;闭环。不到你手写驱动的时间的 1/10，但建立的操作习惯后面每个 Level 都在用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAndroid 系统启动后，用户态的入口是什么？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003einit 进程是所有用户态进程的祖先（PID 1）\u003c/li\u003e\n\u003cli\u003einit 解析 \u003ccode\u003einit.rc\u003c/code\u003e 文件，按规则启动服务\u003c/li\u003e\n\u003cli\u003e厂商自己的服务可以通过 \u003ccode\u003einit_rc\u003c/code\u003e 属性添加到 vendor 分区\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003evendor 分区和 system 分区是什么关系？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003esystem 分区存 Android 通用组件\u003c/li\u003e\n\u003cli\u003evendor 分区存厂商（Amlogic/你的公司）的专有部分\u003c/li\u003e\n\u003cli\u003eTreble 架构强制隔离二者——你的服务应该放 vendor 分区\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e为什么加了服务还要写 SELinux policy？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAndroid 的 SELinux 默认不让任何未定义的进程运行\u003c/li\u003e\n\u003cli\u003e不加 .te 文件，你的服务一启动就被 kernel 杀掉\u003c/li\u003e\n\u003cli\u003e不是 bug，是机制\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eAndroid.bp（Soong 构建）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAOSP 从 Android 7 开始引入 Soong 替代 Makefile，配置文件是 \u003ccode\u003eAndroid.bp\u003c/code\u003e。常用模块类型：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-bp\" data-lang=\"bp\"\u003ecc_binary {           // 编译为可执行文件\n    name: \u0026#34;luojService\u0026#34;,\n    srcs: [\u0026#34;luojService.cpp\u0026#34;],\n    shared_libs: [\n        \u0026#34;liblog\u0026#34;,\n    ],\n    vendor: true,      // 安装到 vendor 分区\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003e\u003ccode\u003evendor: true\u003c/code\u003e 决定了产物的安装路径。\u003c/strong\u003e\u003c/p\u003e","title":"Level 3：加一个 native 服务开机自启动"},{"content":"Level 2：编译自己的固件并烧录 我想做 从源码完整编译出一版固件，烧到板子里，确认跑的是我自己编译的系统。\n先回答三个问题 你的项目用什么方式编译？\n标准 AOSP：source build/envsetup.sh → lunch → make 本项目（ross）：用 build.cfg + ./mk 脚本 两者的区别是什么？ 一个固件由哪些东西组成？\nboot.img（内核 + ramdisk） super.img（system + vendor + product） dtb/dtbo（设备树） 这些镜像文件在 out/target/product/ross/ 下 怎么判断编译成功了？\n最后一行：#### build completed successfully #### 关键镜像文件生成且大小合理 烧录后 ro.build.date 是你刚才的编译时间 需要懂的知识 AOSP 构建系统骨架\n一个 Android 固件的编译最终是一系列 Makefile 规则的执行，但 AOSP 用了一整套包装：\nbuild/envsetup.sh ——设置环境变量，提供 lunch、mmm、make 等命令 lunch ——选择产品+编译类型，本质是设置 TARGET_PRODUCT 和 TARGET_BUILD_VARIANT make ——根据产品配置读取所有 Android.mk/Android.bp，编译出完整系统 在 ross 项目中，build.cfg 代替了 lunch 的手动选择：\n# build.cfg 的内容： PRODUCT_NAME=ross TYPE=userdebug BOARD_COMPILE_ATV=false ./mk ross 就是读取这个文件，自动调用对应的 AOSP 命令。\n几种编译类型\n类型 adb root SELinux 用途 eng 有 宽松 开发调试，最慢 userdebug 有 强制 调试与测试，常用 user 无 强制 发布版 烧录方式\nAmlogic 平台两种主流烧录方式：\nUSB Burning Tool（Windows）：需要 update.img 整包，整个 eMMC 全部擦除重写。适用于第一次烧录或板子变砖时 fastboot（Android 自带）：可以只烧单个分区（如只烧 boot.img），迭代开发时快很多。需要板子进入 fastboot 模式 动手方案 # 第 1 步：确认编译环境 cd ~/android/aml/s905x5/aml-s905x5-androidu-v2/ cat build.cfg # 确认 PRODUCT_NAME=ross ls device/amlogic/ross/ # 板级配置存在 # 第 2 步：开始编译 ./mk ross 2\u0026gt;\u0026amp;1 | tee build.log # 第一次编译 8-16 小时，之后增量编译快很多 # 第 3 步：确认编译产物 ls -lh out/target/product/ross/*.img # 应该能看到 boot.img、super.img、dtb*.img 等 # 第 4 步：烧录 # 方式 A：USB Burning Tool（如果有 update.img） # ./mk ross update # 生成烧录包 # 在 Windows 上打开 USB_Burning_Tool，导入 update.img，按住板子上的 # 升级按键上电，点击\u0026#34;开始\u0026#34; # 方式 B：fastboot（推荐，迭代时快得多） adb reboot bootloader # 如果 adb reboot bootloader 不行，按住板子升级按键上电直接进 fastboot flash boot out/target/product/ross/boot.img fastboot flash super out/target/product/ross/super.img fastboot flash dtb out/target/product/ross/dtb*.img fastboot reboot # 第 5 步：验证 adb shell getprop ro.build.date # 输出应该是你的编译时间 验收清单 项目 验证方式 编译成功 build.log 最后一行 #### build completed successfully #### 烧录后正常开机 adb shell 能进入 是你编译的固件 ro.build.date 是刚才的编译时间，ro.build.fingerprint 包含你的编译信息 dmesg 无核心错误 adb shell dmesg | grep -i \u0026quot;error|fail\u0026quot; 没有关键驱动报错 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-2-%E7%BC%96%E8%AF%91%E7%83%A7%E5%BD%95/","summary":"\u003ch1 id=\"level-2编译自己的固件并烧录\"\u003eLevel 2：编译自己的固件并烧录\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e从源码完整编译出一版固件，烧到板子里，确认跑的是我自己编译的系统。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e你的项目用什么方式编译？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e标准 AOSP：\u003ccode\u003esource build/envsetup.sh\u003c/code\u003e → \u003ccode\u003elunch\u003c/code\u003e → \u003ccode\u003emake\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e本项目（ross）：用 \u003ccode\u003ebuild.cfg\u003c/code\u003e + \u003ccode\u003e./mk\u003c/code\u003e 脚本\u003c/li\u003e\n\u003cli\u003e两者的区别是什么？\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e一个固件由哪些东西组成？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eboot.img（内核 + ramdisk）\u003c/li\u003e\n\u003cli\u003esuper.img（system + vendor + product）\u003c/li\u003e\n\u003cli\u003edtb/dtbo（设备树）\u003c/li\u003e\n\u003cli\u003e这些镜像文件在 \u003ccode\u003eout/target/product/ross/\u003c/code\u003e 下\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e怎么判断编译成功了？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e最后一行：\u003ccode\u003e#### build completed successfully ####\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e关键镜像文件生成且大小合理\u003c/li\u003e\n\u003cli\u003e烧录后 \u003ccode\u003ero.build.date\u003c/code\u003e 是你刚才的编译时间\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eAOSP 构建系统骨架\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e一个 Android 固件的编译最终是一系列 Makefile 规则的执行，但 AOSP 用了一整套包装：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ebuild/envsetup.sh\u003c/code\u003e ——设置环境变量，提供 \u003ccode\u003elunch\u003c/code\u003e、\u003ccode\u003emmm\u003c/code\u003e、\u003ccode\u003emake\u003c/code\u003e 等命令\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003elunch\u003c/code\u003e ——选择产品+编译类型，本质是设置 \u003ccode\u003eTARGET_PRODUCT\u003c/code\u003e 和 \u003ccode\u003eTARGET_BUILD_VARIANT\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emake\u003c/code\u003e ——根据产品配置读取所有 \u003ccode\u003eAndroid.mk\u003c/code\u003e/\u003ccode\u003eAndroid.bp\u003c/code\u003e，编译出完整系统\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在 ross 项目中，\u003ccode\u003ebuild.cfg\u003c/code\u003e 代替了 \u003ccode\u003elunch\u003c/code\u003e 的手动选择：\u003c/p\u003e","title":"Level 2：编译自己的固件并烧录"},{"content":"Level 1：确认板子是活的 我想做 拿到一块 S905X5M 板子，通电，确认它能正常工作，搞清楚\u0026quot;我手里有什么\u0026quot;。\n先回答三个问题 板子需要哪些物理连接才能工作？\n电源（看板子丝印，12V/5V？DC 口还是接线柱？） 串口（TX/RX/GND 三根线，接 TTL-USB 转接板） 显示（HDMI 接显示器） 网络或 USB（用来 adb 连接） 找出你的板子上每个接口的位置 怎么判断板子\u0026quot;活着\u0026quot;？\n串口有输出（bootloader 启动日志） HDMI 有画面（Android 启动动画或桌面） adb 能连上（adb devices 看到设备） 什么是 Android 的板级信息？\nro.board.platform——芯片平台代号（S905X5M 是什么？） ro.build.version.release——Android 版本号 ro.build.fingerprint——完整构建指纹（包含厂商/产品/版本/编译号） ro.build.date——固件编译时间 /proc/version——内核版本和编译工具链 这些信息由谁写入的？→ build.prop 文件 需要懂的知识 adb 基础命令\nadb（Android Debug Bridge）是 Android 调试的核心工具，三个组件：adb client（你输入命令的终端）、adb server（后台进程）、adbd（板子上跑的守护进程）。只需要记住：\nadb shell \u0026lt;cmd\u0026gt; ——在板子上执行命令 adb devices ——查看连接的设备列表 adb push/pull ——传文件 Linux 虚拟文件系统\n/proc/ ——内核暴露的运行信息（进程、内存、版本），不是真实文件 /sys/ ——内核暴露的设备参数，可以读写来配置驱动 getprop ——读取 Android 系统的属性，底层就是读取 / 下的属性文件 分区概念\n板子上的存储（eMMC）被划分为多个分区，每个分区有独立用途：\nboot ——内核 + ramdisk super ——system/vendor/product 的合并分区（Android 10+） vendor ——厂商专有文件和 HAL dtb/dtbo ——设备树 这些分区在 /dev/block/ 下可见。 动手方案 # 第 1 步：接串口线 # 板子上找标注了 TX/RX/GND 的排针，接 TTL-USB 转接板（交叉连接：TX→RX，RX→TX） # 波特率 115200，用 screen/minicom/putty 打开 # 第 2 步：上电，串口会输出 bootloader 日志 # 注意看有没有类似下面的输出： # G12A:BL:... # DDR init complete # U-Boot ... # 第 3 步：等板子启动完成，接网络或 USB，adb 连接 adb devices # 如果显示 \u0026#34;unauthorized\u0026#34;，板子上点允许 # 第 4 步：收集板子信息（记录到笔记里） adb shell getprop ro.board.platform adb shell getprop ro.build.version.release adb shell getprop ro.build.fingerprint adb shell getprop ro.build.date adb shell cat /proc/version adb shell cat /proc/cpuinfo | head -20 # 第 5 步：看分区布局 adb shell cat /proc/partitions adb shell df -h # 第 6 步：找到你的源码里的板级目录 ls ~/android/aml/s905x5/aml-s905x5-androidu-v2/device/amlogic/ross/ # 第 7 步：找设备树文件 find ~/android/aml/s905x5/aml-s905x5-androidu-v2/ -name \u0026#34;*.dts\u0026#34; -path \u0026#34;*s905*\u0026#34; 2\u0026gt;/dev/null | head -5 验收清单 项目 验证方式 串口有完整启动日志 看到 ROM Code→DDR→U-Boot→kernel 输出 adb 能连接 adb shell 进入板子终端 记录了板子的 Android 版本和内核版本 笔记中有 getprop 和 /proc/version 输出 找到板级配置目录 device/amlogic/ross/ 确认存在 找到至少一个 dts 文件 find 返回了有效路径 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-1-%E8%AE%A4%E8%AF%86%E6%9D%BF%E5%AD%90/","summary":"\u003ch1 id=\"level-1确认板子是活的\"\u003eLevel 1：确认板子是活的\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e拿到一块 S905X5M 板子，通电，确认它能正常工作，搞清楚\u0026quot;我手里有什么\u0026quot;。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e板子需要哪些物理连接才能工作？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e电源（看板子丝印，12V/5V？DC 口还是接线柱？）\u003c/li\u003e\n\u003cli\u003e串口（TX/RX/GND 三根线，接 TTL-USB 转接板）\u003c/li\u003e\n\u003cli\u003e显示（HDMI 接显示器）\u003c/li\u003e\n\u003cli\u003e网络或 USB（用来 adb 连接）\u003c/li\u003e\n\u003cli\u003e找出你的板子上每个接口的位置\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e怎么判断板子\u0026quot;活着\u0026quot;？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e串口有输出（bootloader 启动日志）\u003c/li\u003e\n\u003cli\u003eHDMI 有画面（Android 启动动画或桌面）\u003c/li\u003e\n\u003cli\u003eadb 能连上（\u003ccode\u003eadb devices\u003c/code\u003e 看到设备）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e什么是 Android 的板级信息？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ero.board.platform\u003c/code\u003e——芯片平台代号（S905X5M 是什么？）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ero.build.version.release\u003c/code\u003e——Android 版本号\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ero.build.fingerprint\u003c/code\u003e——完整构建指纹（包含厂商/产品/版本/编译号）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ero.build.date\u003c/code\u003e——固件编译时间\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e/proc/version\u003c/code\u003e——内核版本和编译工具链\u003c/li\u003e\n\u003cli\u003e这些信息由谁写入的？→ build.prop 文件\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eadb 基础命令\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eadb（Android Debug Bridge）是 Android 调试的核心工具，三个组件：adb client（你输入命令的终端）、adb server（后台进程）、adbd（板子上跑的守护进程）。只需要记住：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eadb shell \u0026lt;cmd\u0026gt;\u003c/code\u003e ——在板子上执行命令\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eadb devices\u003c/code\u003e ——查看连接的设备列表\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eadb push/pull\u003c/code\u003e ——传文件\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eLinux 虚拟文件系统\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e/proc/\u003c/code\u003e ——内核暴露的运行信息（进程、内存、版本），不是真实文件\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e/sys/\u003c/code\u003e ——内核暴露的设备参数，可以读写来配置驱动\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003egetprop\u003c/code\u003e ——读取 Android 系统的属性，底层就是读取 \u003ccode\u003e/\u003c/code\u003e 下的属性文件\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e分区概念\u003c/strong\u003e\u003c/p\u003e","title":"Level 1：确认板子是活的"},{"content":"实操问题记录 按 target.md 的 Level 对应，只记录实操中遇到的编译/运行错误和异常现象。知识点类内容归属各 Level 文档本身。\nLevel 3：加一个 native 服务开机自启动 对应学习文档：level-3-native服务.md\n[L3-01] checkpolicy unrecognized character 现象： ERROR 'unrecognized character' at token '，指向新建的 .te 文件\n根因： .te 文件是 Windows CRLF（\\r\\n）换行符，Linux 的 checkpolicy 不识别 \\r\n修复： sed -i 's/\\r$//' vendor/giec/common/sepolicy/luojService.te\n验证： cat -A file.te → 看到 $（LF）而非 ^M$（CRLF）\n[L3-02] neverallow 违规：dac_override 现象： neverallow check failed ... allow luojService self:capability { dac_override }\n根因： dac_override（绕过文件权限）被系统核心策略（domain.te:470）全局禁止，任何 domain 不可申请\n修复： 从 .te 中删除 allow luojService self:capability { dac_override };\n启示： 非必要不声明 capability，纯打日志的服务不需要任何 capability\n[L3-03] init domain transition 失败 现象： start luojService 返回 Unable to start service；dmesg 报 File /vendor/bin/luojService(labeled \u0026quot;u:object_r:vendor_file:s0\u0026quot;) has incorrect label or no domain transition\n根因： 只配了 .te 文件（定义 domain/exec_type），缺了 file_contexts 条目（路径→标签映射）。二进制被标记为默认 vendor_file，init 执行它时无法完成 domain transition\n修复： 在 vendor/giec/common/sepolicy/file_contexts 中添加：/vendor/bin/luojService u:object_r:luojService_exec:s0\n验证： adb shell ls -Z /vendor/bin/luojService → 输出应为 u:object_r:luojService_exec:s0\nLevel 4：字符设备驱动 对应学习文档：level-4-字符驱动.md\n[L4-01] vmlinux 依赖导致 .ko 无法链接 现象： make[2]: *** No rule to make target 'vmlinux', needed by 'drivers/misc/simple_char.ko'. Stop.，同时有大量 modpost: ... undefined! 警告\n根因： .ko 文件需要内核的导出符号表（vmlinux.symvers）来解析 register_chrdev、copy_from_user 等符号。必须先有 vmlinux 才能链接模块\n修复： 走 Bazel 构建系统（./build.sh -k），不要直接用 make 编模块\n[L4-02] Bazel 编译报错：源码树不干净 现象： ./build.sh -kj200 报 The source tree is not clean, please run 'make mrproper'，Bazel 构建失败\n根因： 在 common/common14-5.15/common/ 下直接执行了 make 或 make menuconfig，生成了 .config、include/config/、drivers/misc/*.o 等中间文件。Bazel 构建系统要求源码树绝对干净，检测到这些残留文件后拒绝编译\n修复： cd common/common14-5.15/common \u0026amp;\u0026amp; make mrproper 清除所有编译中间文件，再重新 ./build.sh -kj200\n教训： 用 Bazel 构建时不要在 common/ 下执行 make 或 make menuconfig。内核配置通过修改 arch/arm64/configs/amlogic_gki.fragment 实现\n验证： 执行 ./build.sh -k 前确认源码树无 .config、.o 等未跟踪文件\n[L4-03] 改错了 fragment 路径，Bazel 未采用配置 现象： 在 amlogic_gki.fragment 中加了 CONFIG_SIMPLE_CHAR=m，但编译后 .config 仍是 # CONFIG_SIMPLE_CHAR is not set，simple_char.ko 未生成\n根因： 该项目存在两个同名的 amlogic_gki.fragment，传统 make 和 Bazel 各用一个：\n路径 被谁用 common/arch/arm64/configs/amlogic_gki.fragment 传统 build.sh / make common/common_drivers/arch/arm64/configs/amlogic_gki.fragment Bazel（build.config.amlogic.bazel） 我们改了第一个，但 Bazel 引用的是第二个。Bazel 通过 common_drivers/build.config.amlogic.bazel 指定了厂商驱动目录的配置路径。\n修复： 将 CONFIG_SIMPLE_CHAR=m 添加到正确的文件 common/common_drivers/arch/arm64/configs/amlogic_gki.fragment\n验证： 通过以下 4 个文件确认 Bazel 使用的 fragment 路径：\nBUILD.bazel:811 → common_drivers/amlogic.bzl ↓ build_config build.config.amlogic.bazel:7 → project/build.config.gki10 ↓ COMMON_DRIVERS_DIR common_drivers build.config.amlogic.bazel:31 → ${KERNEL_DIR}/${COMMON_DRIVERS_DIR}/arch/arm64/configs/amlogic_gki.fragment → 实际路径：common/common_drivers/arch/arm64/configs/amlogic_gki.fragment 教训： 修改内核配置前，先确认 build.config.amlogic.bazel 中引用的 fragment 路径，而非按直觉找 common/arch/arm64/ 下的同名文件\n[L4-04] 模块编译成功但未被拷贝到 dist 目录 现象： ./build.sh -kj200 编译完成，simple_char.o 编译成功，但最终报 ERROR: modules are built but not copied，out/android14-5.15/dist/ 下没有 simple_char.ko\n根因： Bazel 编译模块后，需要通过 modules.bzl 中的 AMLOGIC_COMMON_MODULES 列表声明哪些 .ko 文件需要拷贝到 dist 目录。未声明的模块即使编译成功也不会被拷贝\n修复： 在 common/common_drivers/modules.bzl 的 AMLOGIC_COMMON_MODULES 列表中添加 \u0026quot;drivers/misc/simple_char.ko\u0026quot;（保持字母序）\n验证： 重新 ./build.sh -kj200 后检查 out/android14-5.15/dist/simple_char.ko 存在\n[L4-05] dev_read() 永不返回 EOF 导致 cat 死循环 现象： cat /dev/simple_char 触发 ~40 次 read() 4096 bytes，无限循环读取同一段数据\n根因： dev_read() 没有使用 offset 参数追踪读取位置，也没有在数据读完后返回 0（EOF）。VFS 规范要求：当 *offset \u0026gt;= data_len 时必须返回 0，否则 cat 等用户态工具认为还有数据可读，不断重试\n修复： 在 dev_read() 中加入 offset 逻辑：\n用 strlen(kernel_buffer) 算出实际数据长度 *offset \u0026gt;= data_len 时返回 0 用 kernel_buffer + *offset 作为 copy_to_user 的源地址 拷贝成功后 *offset += len 验证： cat /dev/simple_char 应打印一次数据后正常退出（而非循环打印）\nLevel 5：操作 GPIO 对应学习文档：level-5-GPIO.md\n[L5-01] /sys/kernel/debug/gpio 不存在 现象： 板子上 /sys/kernel/debug/ 目录下没有 gpio 文件/目录\n根因： 内核配置中 # CONFIG_GPIO_SYSFS is not set，旧的 sysfs GPIO 接口已被禁用（kernel 5.15 默认行为）。虽然 CONFIG_DEBUG_FS=y 且 gpiolib 理论上会创建 /sys/kernel/debug/gpio，但实际可能需先挂载 debugfs 或加载 gpio 驱动模块\n排查步骤：\nadb shell mount | grep debugfs — 确认 debugfs 是否挂载；若未挂载：adb shell mount -t debugfs none /sys/kernel/debug adb shell lsmod | grep gpio — 确认 amlogic-gpio.ko 是否加载；若未加载：adb shell modprobe amlogic-gpio 若 debugfs gpio 文件确实不存在，改用 gpio 字符设备接口 替代方案（推荐）： 内核 5.15 已启用 CONFIG_GPIO_CDEV=y，使用 libgpiod 工具：\nadb shell gpiodetect # 列出所有 gpiochip adb shell gpioinfo # 查看所有 GPIO 的详细信息 adb shell gpioset gpiochip0 \u0026lt;pin\u0026gt;=1 # 设置输出 adb shell gpioget gpiochip0 \u0026lt;pin\u0026gt; # 读取输入 参考代码路径：\nGPIO 控制器节点：common/common_drivers/arch/arm64/boot/dts/amlogic/meson-s7d.dtsi（S7D） GPIO pin 定义：common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h（83 pins，GPIOD_0=24 ~ GPIOZ_12=80） gpiolib debugfs 创建：common/drivers/gpio/gpiolib.c:4509-4515（debugfs_create_file(\u0026quot;gpio\u0026quot;, ...) 在 #ifdef CONFIG_DEBUG_FS 下） 构建配置链：common/common_drivers/build.config.amlogic.bazel:33（合并 gki_defconfig + amlogic_gki.fragment + amlogic_gki.10 + amlogic_gki.debug） 板子 LED 硬件信息： S905X5M (S7D) 参考板的 RGB LED 通过 AW9523B I2C GPIO 扩展芯片控制，不是直接接在 SoC GPIO 上。原理图中 GPIOD_4 标为 FUN_LED，对应全局编号 28（meson-s7d-gpio.h），可直接用于 GPIO 驱动练习\n[L5-02] insmod fun_led.ko 报 -517（-EPROBE_DEFER），GPIOD_4 已被 leds-gpio 占用 现象： insmod fun_led.ko 成功加载模块，但 dmesg 报 gpio_request failed, ret=-517（-EPROBE_DEFER）。LED 在重启时闪烁红灯，说明硬件连接正常。\n根因： 两层拒绝。\n第一层（DTS 资源冲突，根本原因）： GPIOD_4 已在板级 DTS s7d_s905x5m_bm201.dts:224-228 中声明为 gpio-leds 设备的子节点 sys_led_red。内核 leds-gpio 驱动（drivers/leds/leds-gpio.c）通过 devm_fwnode_get_gpiod_from_child() → gpiod_get() 申请了该 pin。fun_led.ko 的 gpio_request(28) 是重复申请同一 pin。\n第二层（GPIO chip owner 模块检查，导致报 -517 而非 -EBUSY）： gpiod_request() 返回值初始值就是 -EPROBE_DEFER（gpiolib.c:1988）。在 GKI 架构下，gpio_chip 的 owner 指向 vendor 模块，try_module_get(gdev-\u0026gt;owner) 返回 false 时维持 -517，不会进入 gpiod_request_commit() → 不会触发\u0026quot;已占用\u0026quot;的 -EBUSY 检查。\n正确的控制方式（现成方案，零代码）：\necho 1 \u0026gt; /sys/class/leds/sys_led_red/brightness # LED 亮 echo 0 \u0026gt; /sys/class/leds/sys_led_red/brightness # LED 灭 echo heartbeat \u0026gt; /sys/class/leds/sys_led_red/trigger # 心跳闪烁 完整分析写入： level-5-GPIO.md → \u0026ldquo;实战：板子上已有的 DTS + gpio-leds 方案\u0026rdquo;\n关键代码路径：\nDTS SoC 级：common/common_drivers/arch/arm64/boot/dts/amlogic/meson-s7d.dtsi:456-469（bank@4000 控制器节点） DTS 板级：common/common_drivers/arch/arm64/boot/dts/amlogic/s7d_s905x5m_bm201.dts:216-229（gpio_leds { sys_led_red } 节点） 驱动匹配：common/drivers/leds/leds-gpio.c:187-190（of_gpio_leds_match[]） GPIO 获取：common/drivers/leds/leds-gpio.c:154（devm_fwnode_get_gpiod_from_child()） LED 初始化：common/drivers/leds/leds-gpio.c:75-123（create_gpio_led()） sysfs 接口：common/drivers/leds/led-class.c:38-65（brightness_store()） GPIO 调用链：common/drivers/gpio/gpiolib.c → common/common_drivers/drivers/gpio/pinctrl/pinctrl-meson.c → 硬件寄存器 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/debug/","summary":"\u003ch1 id=\"实操问题记录\"\u003e实操问题记录\u003c/h1\u003e\n\u003cp\u003e按 \u003ccode\u003etarget.md\u003c/code\u003e 的 Level 对应，只记录实操中遇到的\u003cstrong\u003e编译/运行错误\u003c/strong\u003e和\u003cstrong\u003e异常现象\u003c/strong\u003e。知识点类内容归属各 Level 文档本身。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"level-3加一个-native-服务开机自启动\"\u003eLevel 3：加一个 native 服务开机自启动\u003c/h2\u003e\n\u003cp\u003e对应学习文档：\u003ca href=\"level-3-native%E6%9C%8D%E5%8A%A1.md\"\u003elevel-3-native服务.md\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"l3-01-checkpolicy-unrecognized-character\"\u003e[L3-01] checkpolicy unrecognized character\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e现象：\u003c/strong\u003e \u003ccode\u003eERROR 'unrecognized character' at token '\u003c/code\u003e，指向新建的 \u003ccode\u003e.te\u003c/code\u003e 文件\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e根因：\u003c/strong\u003e \u003ccode\u003e.te\u003c/code\u003e 文件是 Windows CRLF（\u003ccode\u003e\\r\\n\u003c/code\u003e）换行符，Linux 的 checkpolicy 不识别 \u003ccode\u003e\\r\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e修复：\u003c/strong\u003e \u003ccode\u003esed -i 's/\\r$//' vendor/giec/common/sepolicy/luojService.te\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e验证：\u003c/strong\u003e \u003ccode\u003ecat -A file.te\u003c/code\u003e → 看到 \u003ccode\u003e$\u003c/code\u003e（LF）而非 \u003ccode\u003e^M$\u003c/code\u003e（CRLF）\u003c/p\u003e\n\u003ch3 id=\"l3-02-neverallow-违规dac_override\"\u003e[L3-02] neverallow 违规：dac_override\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e现象：\u003c/strong\u003e \u003ccode\u003eneverallow check failed ... allow luojService self:capability { dac_override }\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e根因：\u003c/strong\u003e \u003ccode\u003edac_override\u003c/code\u003e（绕过文件权限）被系统核心策略（\u003ccode\u003edomain.te:470\u003c/code\u003e）全局禁止，任何 domain 不可申请\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e修复：\u003c/strong\u003e 从 \u003ccode\u003e.te\u003c/code\u003e 中删除 \u003ccode\u003eallow luojService self:capability { dac_override };\u003c/code\u003e\u003c/p\u003e","title":"实操问题记录"},{"content":"医保 个人账户与统筹账户 2025 年深圳职工医保一档（单位 5%、个人 2%；基数上下限 6733–33666 元）\n杰科：6777\n个账：基数 ×2%，个人缴费全额划入，单位不划入。 统筹：单位 5%+（总缴费−个账），即总缴费中扣除个账后全部入统筹。 基数超上限按上限算，低于下限按下限算。 待遇 待遇类型 起付线 支付比例（按机构） 年度限额 备注 普通门诊 无 一级以下 75%、二级 65%、三级 55% 10478.4 元；二级以上 / 专科 5239.2 元 目录内，超限额个账 / 自付 住院 一级以下 200、二级 400、三级 600；二次及以上减半 一级以下 94%、二级 92%、三级 90% 基本医保统筹 + 大病保险，额度高 目录内，起付线后按比例 门诊特定病种 按病种设起付 / 限额 按病种与机构分级 按病种单独核算 另行申请认定，额度独立 报销计算步骤 筛目录：仅医保目录内药品、诊疗、耗材可纳入报销，自费项目不纳入。 扣起付：住院先扣对应起付线；门诊无起付线。 算统筹：（目录内费用−起付线）× 对应机构比例 = 统筹记账；不得超当期限额。 付自付：统筹未覆盖部分（起付线、比例外、超限额、自费），优先用个账支付，余额不足自掏腰包。 实例 场景 1：三级医院普通门诊，目录内费用 8000 元。统筹记账 = 8000×55%=4400 元，未超二级以上 / 专科限额 5239.2 元；自付 3600 元，可刷个账 155.56 元 / 月累计余额支付。 场景 2：二级医院首次住院，目录内费用 50000 元。统筹记账 =（50000−400）×92%=46032 元；自付 3968 元，个账余额可抵扣。 场景 3：二级医院门诊超限额，目录内费用 6000 元。统筹最多记账 5239.2 元；超限额 760.8 元 + 比例外自付，用个账 / 自付。 额度计算 统筹账户额度分三类口径计算：\n普通门诊统筹（按比例与年度限额） 住院 / 门特统筹（起付线后按比例，受连续参保年限封顶） 大病保险（基本统筹封顶后衔接） 基数：2025 深圳一档按偿付基数 174640 元 / 年执行\n类别 适用 限额 / 规则 计算要点 普通门诊统筹 在职 / 退休 在职：10478.4 元（含二级以上 / 专科 5239.2 元）；退休：12224.8 元（含二级以上 / 专科 6112.4 元） 目录内 × 比例≤限额；机构等级影响比例 基本统筹累计（住院 / 门特等） 在职 / 退休 按连续参保年限：≤6 月 1 倍（17.46 万）；≥72 月 6 倍（104.78 万） 起付线后目录内 × 比例≤当期限额 大病保险 在职 / 退休 基本统筹封顶后自动衔接 按大病起付线、比例、限额分担 关于大病保险的自动衔接\n也就是统筹上限之后，超过大病保险的起付线按比例报销\n例如：\n情景：\n​\t假设连续参保≥72 个月，基本统筹年度限额约 104.78 万元，大病保险起付线 1 万元，分段比例 1 万 —3 万 70%、3 万以上 80%，大病年度限额按连续参保年限核定。\n结果：\n年度内住院 / 门特合规费用经基本统筹报销至 104.78 万元，基本统筹不再支付。 后续合规费用与前期自付部分累计，达到大病起付线 1 万元后，按对应比例报销。 出院结算时，系统自动算完基本统筹与大病保险，你仅付剩余自付 / 自费，无需单独申请。 养老金 作用： 达到法定退休年龄、满足缴费年限要求后，返现\n计算：深圳职工养老保险缴费比例（2025 年）：个人缴 8%，单位缴 14%（深户）/13%（非深户），其中 个人缴费的 8% 全部计入个人账户，单位缴费的部分全部进入统筹基金（不进你的个人账户）。（基数上下限 6733–33666 元，社保基数统一）\n退休后：每月领取=基础养老金 + 个人账户养老金 基础养老金 公式：基础养老金 = (计发基数 + 本人指数化月平均缴费工资) ÷ 2 × 累计缴费年限 × 1%； 其中本人指数化月平均缴费工资 = 计发基数 × 本人平均缴费指数。 关键参数 计发基数：当地人社年度公布，深圳 2025 年企业职工计发基数 11293 元 / 月。 平均缴费指数：历年缴费基数 ÷ 对应年度计发基数的平均值，常见区间 0.6–3.0，缴得越高指数越高。 累计缴费年限：含实际缴费与视同缴费年限，不满 1 年按月份折算。 示例（深圳，60 岁退休，计发基数 11293，指数 1.0，缴费 30 年）：11293×(1+1.0)÷2×30×1% = 3387.9 元 / 月。 PS：\n​\t基础养老金分为职工和居民\n​\t居民基础养老金由地方规定\n全国保底 全国统一底线 143 各地不得低于此标准 中西部常规 四川、河南等 160–193 含省级补贴，部分县区再叠加 东部普通 山东、福建等 190–247 多含年限 / 高龄加发 省内发达地市 苏州、杭州等 350–705 地市加码，差异显著 一线 / 头部 深圳、上海、北京 533（深户籍满 8 年）、1555（沪）、998（京） 深含户籍 / 年限 / 高龄加发；沪、京为全市统一高标 个人账户养老金 公式：个人账户养老金 = 退休时个人账户累计储存额（含利息）÷ 对应退休年龄的计发月数。 关键参数 账户储存额：个人缴费按基数 8% 计入，逐年复利计息，记账利率年度公布。 计发月数（常用）：50 岁 195、55 岁 170、60 岁 139；年龄越大，计发月数越少，月领越高。 示例（账户余额 34 万元，60 岁退休）：340000 ÷ 139 ≈ 2446 元 / 月。 公积金 额度 基数 * （5%~12%），双边的，公司缴纳的也会进入个人账户\n例如基数3000，5%，每月公积金3000 * 5% =150 *2 = 300\n公积金贷款 需要声明的一点是贷款的额度跟 个人账户余额、存缴年限以及每月存缴 相关\n还款不一定强制使用公积金余额，余额只是衡量贷款额度的一个标准\n以重庆为例，其政策计算公式，贷款额度为 （个人账户 * 25），最高80W，夫妻的话则是（总余额 * 25），最高120w；此外最高不超过房总价的80%\n","permalink":"https://luojmarkdown.pages.dev/posts/%E6%97%A5%E5%B8%B8%E7%9F%A5%E8%AF%86/%E4%BA%94%E9%99%A9%E4%B8%80%E9%87%91/","summary":"\u003ch2 id=\"医保\"\u003e医保\u003c/h2\u003e\n\u003ch3 id=\"个人账户与统筹账户\"\u003e个人账户与统筹账户\u003c/h3\u003e\n\u003cp\u003e2025 年深圳职工医保一档（单位 5%、个人 2%；\u003cstrong\u003e基数上下限 6733–33666 元\u003c/strong\u003e）\u003c/p\u003e\n\u003cp\u003e杰科：6777\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e个账：基数 ×2%，个人缴费全额划入，单位不划入。\u003c/li\u003e\n\u003cli\u003e统筹：单位 5%+（总缴费−个账），即总缴费中扣除个账后全部入统筹。\u003c/li\u003e\n\u003cli\u003e基数超上限按上限算，低于下限按下限算。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"待遇\"\u003e待遇\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e待遇类型\u003c/th\u003e\n          \u003cth\u003e起付线\u003c/th\u003e\n          \u003cth\u003e支付比例（按机构）\u003c/th\u003e\n          \u003cth\u003e年度限额\u003c/th\u003e\n          \u003cth\u003e备注\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e普通门诊\u003c/td\u003e\n          \u003ctd\u003e无\u003c/td\u003e\n          \u003ctd\u003e一级以下 75%、二级 65%、三级 55%\u003c/td\u003e\n          \u003ctd\u003e10478.4 元；二级以上 / 专科 5239.2 元\u003c/td\u003e\n          \u003ctd\u003e目录内，超限额个账 / 自付\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e住院\u003c/td\u003e\n          \u003ctd\u003e一级以下 200、二级 400、三级 600；二次及以上减半\u003c/td\u003e\n          \u003ctd\u003e一级以下 94%、二级 92%、三级 90%\u003c/td\u003e\n          \u003ctd\u003e基本医保统筹 + 大病保险，额度高\u003c/td\u003e\n          \u003ctd\u003e目录内，起付线后按比例\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e门诊特定病种\u003c/td\u003e\n          \u003ctd\u003e按病种设起付 / 限额\u003c/td\u003e\n          \u003ctd\u003e按病种与机构分级\u003c/td\u003e\n          \u003ctd\u003e按病种单独核算\u003c/td\u003e\n          \u003ctd\u003e另行申请认定，额度独立\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"报销计算步骤\"\u003e报销计算步骤\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e筛目录：仅医保目录内药品、诊疗、耗材可纳入报销，自费项目不纳入。\u003c/li\u003e\n\u003cli\u003e扣起付：住院先扣对应起付线；门诊无起付线。\u003c/li\u003e\n\u003cli\u003e算统筹：（目录内费用−起付线）× 对应机构比例 = 统筹记账；不得超当期限额。\u003c/li\u003e\n\u003cli\u003e付自付：统筹未覆盖部分（起付线、比例外、超限额、自费），优先用个账支付，余额不足自掏腰包。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"实例\"\u003e实例\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e场景 1：三级医院普通门诊，目录内费用 8000 元。统筹记账 = 8000×55%=4400 元，未超二级以上 / 专科限额 5239.2 元；自付 3600 元，可刷个账 155.56 元 / 月累计余额支付。\u003c/li\u003e\n\u003cli\u003e场景 2：二级医院首次住院，目录内费用 50000 元。统筹记账 =（50000−400）×92%=46032 元；自付 3968 元，个账余额可抵扣。\u003c/li\u003e\n\u003cli\u003e场景 3：二级医院门诊超限额，目录内费用 6000 元。统筹最多记账 5239.2 元；超限额 760.8 元 + 比例外自付，用个账 / 自付。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"额度计算\"\u003e额度计算\u003c/h3\u003e\n\u003cp\u003e统筹账户额度分三类口径计算：\u003c/p\u003e","title":"五险一金"},{"content":"Level 6 总结与扩展：逐函数拆解 GPIO 按键驱动 本文是 level-6-按键.md 的延伸，以 Amlogic gpio_keypad.c（557 行）为样本，按照代码的物理顺序，逐函数解释它干了什么。\n读完本文你会：能自己读懂一个陌生驱动，知道每个函数在做什么。\n驱动源文件：common/common_drivers/drivers/input/keyboard/gpio_keypad.c\n代码总览（完整源码） 以下是 gpio_keypad.c 的完整代码。每个函数后面跟着逐行解释。\n// SPDX-License-Identifier: (GPL-2.0+ OR MIT) /* * Copyright (c) 2019 Amlogic, Inc. All rights reserved. */ #include \u0026lt;linux/module.h\u0026gt; #include \u0026lt;linux/init.h\u0026gt; #include \u0026lt;linux/types.h\u0026gt; #include \u0026lt;linux/string.h\u0026gt; #include \u0026lt;linux/kstrtox.h\u0026gt; #include \u0026lt;linux/input.h\u0026gt; #include \u0026lt;linux/platform_device.h\u0026gt; #include \u0026lt;linux/of.h\u0026gt; #include \u0026lt;linux/gpio/consumer.h\u0026gt; #include \u0026lt;linux/interrupt.h\u0026gt; #include \u0026lt;linux/slab.h\u0026gt; #include \u0026lt;linux/amlogic/pm.h\u0026gt; #include \u0026lt;linux/irq.h\u0026gt; #include \u0026lt;linux/amlogic/power_domain.h\u0026gt; #include \u0026lt;linux/amlogic/gpiolib.h\u0026gt; #define DEFAULT_SCAN_PERION\t20 #define\tDEFAULT_POLL_MODE\t0 #define KEY_JITTER_COUNT\t1 struct pin_desc { int current_status; struct gpio_desc *desc; int irq_num; u32 code; u32 key_type; const char *name; int count; bool ignore; }; struct gpio_keypad { int key_size; int use_irq;/* 1:irq mode ; 0:polling mode */ int scan_period; struct pin_desc *key; struct pin_desc *current_key; struct timer_list polling_timer; struct input_dev *input_dev; struct class kp_class; struct class resetkey_class; unsigned int reset_count; }; static irqreturn_t gpio_irq_handler(int irq, void *data) { struct gpio_keypad *keypad; keypad = (struct gpio_keypad *)data; mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(20)); return IRQ_HANDLED; } static void report_key_code(struct gpio_keypad *keypad, int gpio_val) { struct pin_desc *key = keypad-\u0026gt;current_key; if (key-\u0026gt;count \u0026gt;= KEY_JITTER_COUNT) { key-\u0026gt;current_status = gpio_val; if (key-\u0026gt;current_status) { input_event(keypad-\u0026gt;input_dev, key-\u0026gt;key_type, key-\u0026gt;code, 0); dev_dbg(\u0026amp;keypad-\u0026gt;input_dev-\u0026gt;dev, \u0026#34;key %d up.\\n\u0026#34;, key-\u0026gt;code); } else { input_event(keypad-\u0026gt;input_dev, key-\u0026gt;key_type, key-\u0026gt;code, 1); // Count key press if (strcmp(key-\u0026gt;name, \u0026#34;bluetooth\u0026#34;) == 0) { keypad-\u0026gt;reset_count++; } dev_dbg(\u0026amp;keypad-\u0026gt;input_dev-\u0026gt;dev, \u0026#34;key %d down.\\n\u0026#34;, key-\u0026gt;code); } input_sync(keypad-\u0026gt;input_dev); key-\u0026gt;count = 0; } } static void polling_timer_handler(struct timer_list *t) { int i; int gpio_val; int is_pressing = 0; struct gpio_keypad *keypad = from_timer(keypad, t, polling_timer); if (keypad-\u0026gt;use_irq) {/* irq mode */ for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { gpio_val = gpiod_get_value(keypad-\u0026gt;key[i].desc); gpio_val |= keypad-\u0026gt;key[i].ignore; if (keypad-\u0026gt;key[i].current_status != gpio_val) { keypad-\u0026gt;key[i].count++; keypad-\u0026gt;current_key = \u0026amp;keypad-\u0026gt;key[i]; report_key_code(keypad, gpio_val); } else { keypad-\u0026gt;key[i].count = 0; } if (gpio_val == 0 || keypad-\u0026gt;key[i].current_status == 0) is_pressing = 1; } if (is_pressing) mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(keypad-\u0026gt;scan_period)); } else {/* polling mode */ for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { gpio_val = gpiod_get_value(keypad-\u0026gt;key[i].desc); gpio_val |= keypad-\u0026gt;key[i].ignore; if (keypad-\u0026gt;key[i].current_status != gpio_val) { keypad-\u0026gt;key[i].count++; keypad-\u0026gt;current_key = \u0026amp;keypad-\u0026gt;key[i]; report_key_code(keypad, gpio_val); } else { keypad-\u0026gt;key[i].count = 0; } } mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(keypad-\u0026gt;scan_period)); } } static ssize_t table_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); int i; int len = 0; for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { len += sprintf(buf + len, \u0026#34;[%d]: name = %-21s status = %-5d\\n\u0026#34;, i, keypad-\u0026gt;key[i].name, keypad-\u0026gt;key[i].current_status); } return len; } static ssize_t ignore_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); int index; int len = 0; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) { len += sysfs_emit_at(buf, len, \u0026#34;%s,%d: %c\\n\u0026#34;, keypad-\u0026gt;key[index].name, keypad-\u0026gt;key[index].code, keypad-\u0026gt;key[index].ignore ? \u0026#39;Y\u0026#39; : \u0026#39;N\u0026#39;); } return len; } static ssize_t ignore_store(struct class *cls, struct class_attribute *attr, const char *buf, size_t count) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); struct device *dev = keypad-\u0026gt;input_dev-\u0026gt;dev.parent; char nbuf[128]; char tmp[32]; int index; char *value = nbuf; char *name; bool ignore; bool found; if (count \u0026gt; sizeof(nbuf)) { dev_err(dev, \u0026#34;data is too long\\n\u0026#34;); return -EINVAL; } memcpy(nbuf, buf, count); strreplace(nbuf, \u0026#39;\\n\u0026#39;, \u0026#39;\\0\u0026#39;); name = strsep(\u0026amp;value, \u0026#34; \u0026#34;); if (!name) goto err; if (!strcmp(\u0026#34;all\u0026#34;, name)) { for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) keypad-\u0026gt;key[index].ignore = true; dev_dbg(dev, \u0026#34;ignore all keys\\n\u0026#34;); } else if (!strcmp(\u0026#34;none\u0026#34;, name)) { for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) keypad-\u0026gt;key[index].ignore = false; dev_dbg(dev, \u0026#34;enable all keys\\n\u0026#34;); } else { if (!value) goto err; if (kstrtobool(value, \u0026amp;ignore)) { dev_err(dev, \u0026#34;invalid \u0026#39;%s\u0026#39;, please use Y/N instead\\n\u0026#34;, value); return -EINVAL; } /* search key */ found = false; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) { snprintf(tmp, sizeof(tmp), \u0026#34;%s,%d\u0026#34;, keypad-\u0026gt;key[index].name, keypad-\u0026gt;key[index].code); if (!strcmp(tmp, name)) { found = true; break; } } if (!found) { dev_err(dev, \u0026#34;invalid key \u0026#39;%s\u0026#39;\\n\u0026#34;, name); return -EINVAL; } keypad-\u0026gt;key[index].ignore = ignore; dev_dbg(dev, \u0026#34;ignore %s: %s\\n\u0026#34;, name, value); } return count; err: dev_err(dev, \u0026#34;the correct format is: [name],[code] [ignore]\\n\u0026#34;); return -EINVAL; } static CLASS_ATTR_RO(table); static CLASS_ATTR_RW(ignore); static struct attribute *meson_gpiokey_attrs[] = { \u0026amp;class_attr_table.attr, \u0026amp;class_attr_ignore.attr, NULL }; ATTRIBUTE_GROUPS(meson_gpiokey); // Add show function for reset count static ssize_t count_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, resetkey_class); return sprintf(buf, \u0026#34;%u\\n\u0026#34;, keypad-\u0026gt;reset_count); } static CLASS_ATTR_RO(count); static struct attribute *resetkey_attrs[] = { \u0026amp;class_attr_count.attr, NULL }; ATTRIBUTE_GROUPS(resetkey); static int meson_gpio_kp_probe(struct platform_device *pdev) { struct gpio_desc *desc; int ret, i; struct input_dev *input_dev; struct gpio_keypad *keypad; struct irq_desc *d; unsigned int number; int wakeup_source = 0; int register_flag = 0; if (!(pdev-\u0026gt;dev.of_node)) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;pdev-\u0026gt;dev.of_node == NULL!\\n\u0026#34;); return -EINVAL; } keypad = devm_kzalloc(\u0026amp;pdev-\u0026gt;dev, sizeof(struct gpio_keypad), GFP_KERNEL); if (!keypad) return -EINVAL; ret = of_property_read_u32(pdev-\u0026gt;dev.of_node, \u0026#34;detect_mode\u0026#34;, \u0026amp;keypad-\u0026gt;use_irq); if (ret) /* The default mode is polling. */ keypad-\u0026gt;use_irq = DEFAULT_POLL_MODE; ret = of_property_read_u32(pdev-\u0026gt;dev.of_node, \u0026#34;scan_period\u0026#34;, \u0026amp;keypad-\u0026gt;scan_period); if (ret) /* he default scan period is 20. */ keypad-\u0026gt;scan_period = DEFAULT_SCAN_PERION; if (of_property_read_bool(pdev-\u0026gt;dev.of_node, \u0026#34;wakeup-source\u0026#34;)) wakeup_source = 1; ret = of_property_read_u32(pdev-\u0026gt;dev.of_node, \u0026#34;key_num\u0026#34;, \u0026amp;keypad-\u0026gt;key_size); if (ret) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;failed to get key_num!\\n\u0026#34;); return -EINVAL; } keypad-\u0026gt;key = devm_kzalloc(\u0026amp;pdev-\u0026gt;dev, (keypad-\u0026gt;key_size) * sizeof(*keypad-\u0026gt;key), GFP_KERNEL); if (!(keypad-\u0026gt;key)) return -EINVAL; for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { /* get all gpio desc. */ desc = devm_gpiod_get_index(\u0026amp;pdev-\u0026gt;dev, \u0026#34;key\u0026#34;, i, GPIOD_IN); if (IS_ERR_OR_NULL(desc)) return -EINVAL; keypad-\u0026gt;key[i].desc = desc; /* The gpio default is high level. */ keypad-\u0026gt;key[i].current_status = 1; ret = of_property_read_u32_index(pdev-\u0026gt;dev.of_node, \u0026#34;key_code\u0026#34;, i, \u0026amp;keypad-\u0026gt;key[i].code); if (ret \u0026lt; 0) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;find key_code=%d finished\\n\u0026#34;, i); return -EINVAL; } ret = of_property_read_u32_index(pdev-\u0026gt;dev.of_node, \u0026#34;key_type\u0026#34;, i, \u0026amp;keypad-\u0026gt;key[i].key_type); if (ret) keypad-\u0026gt;key[i].key_type = EV_KEY; ret = of_property_read_string_index(pdev-\u0026gt;dev.of_node, \u0026#34;key_name\u0026#34;, i, \u0026amp;keypad-\u0026gt;key[i].name); if (ret \u0026lt; 0) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;find key_name=%d finished\\n\u0026#34;, i); return -EINVAL; } gpiod_direction_input(keypad-\u0026gt;key[i].desc); gpiod_set_pull(keypad-\u0026gt;key[i].desc, GPIOD_PULL_UP); } keypad-\u0026gt;kp_class.name = \u0026#34;gpio_keypad\u0026#34;; keypad-\u0026gt;kp_class.owner = THIS_MODULE; keypad-\u0026gt;kp_class.class_groups = meson_gpiokey_groups; ret = class_register(\u0026amp;keypad-\u0026gt;kp_class); if (ret) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;fail to create gpio keypad class.\\n\u0026#34;); return -EINVAL; } // After registering kp_class, add resetkey class registration keypad-\u0026gt;resetkey_class.name = \u0026#34;resetkey\u0026#34;; keypad-\u0026gt;resetkey_class.owner = THIS_MODULE; keypad-\u0026gt;resetkey_class.class_groups = resetkey_groups; keypad-\u0026gt;reset_count = 0; ret = class_register(\u0026amp;keypad-\u0026gt;resetkey_class); if (ret) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;fail to create resetkey class.\\n\u0026#34;); class_unregister(\u0026amp;keypad-\u0026gt;kp_class); return -EINVAL; } /* input */ input_dev = input_allocate_device(); if (!input_dev) return -EINVAL; for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { input_set_capability(input_dev, keypad-\u0026gt;key[i].key_type, keypad-\u0026gt;key[i].code); dev_dbg(\u0026amp;pdev-\u0026gt;dev, \u0026#34;%s key(%d) type(0x%x) registered.\\n\u0026#34;, keypad-\u0026gt;key[i].name, keypad-\u0026gt;key[i].code, keypad-\u0026gt;key[i].key_type); } input_dev-\u0026gt;name = \u0026#34;gpio_keypad\u0026#34;; input_dev-\u0026gt;phys = \u0026#34;gpio_keypad/input0\u0026#34;; input_dev-\u0026gt;dev.parent = \u0026amp;pdev-\u0026gt;dev; input_dev-\u0026gt;id.bustype = BUS_ISA; input_dev-\u0026gt;id.vendor = 0x0001; input_dev-\u0026gt;id.product = 0x0001; input_dev-\u0026gt;id.version = 0x0100; input_dev-\u0026gt;rep[REP_DELAY] = 0xffffffff; input_dev-\u0026gt;rep[REP_PERIOD] = 0xffffffff; input_dev-\u0026gt;keycodesize = sizeof(unsigned short); input_dev-\u0026gt;keycodemax = 0x1ff; keypad-\u0026gt;input_dev = input_dev; ret = input_register_device(keypad-\u0026gt;input_dev); if (ret \u0026lt; 0) { input_free_device(keypad-\u0026gt;input_dev); return -EINVAL; } platform_set_drvdata(pdev, keypad); timer_setup(\u0026amp;keypad-\u0026gt;polling_timer, polling_timer_handler, 0); if (keypad-\u0026gt;use_irq) { for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { keypad-\u0026gt;key[i].count = 0; keypad-\u0026gt;key[i].irq_num = gpiod_to_irq(keypad-\u0026gt;key[i].desc); ret = devm_request_irq(\u0026amp;pdev-\u0026gt;dev, keypad-\u0026gt;key[i].irq_num, gpio_irq_handler, IRQF_TRIGGER_FALLING, \u0026#34;gpio_keypad\u0026#34;, keypad); if (ret) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;Requesting irq failed!\\n\u0026#34;); input_free_device(keypad-\u0026gt;input_dev); del_timer(\u0026amp;keypad-\u0026gt;polling_timer); return -EINVAL; } if (keypad-\u0026gt;key[i].code == KEY_POWER) { d = irq_to_desc(keypad-\u0026gt;key[i].irq_num); if (d) { number = d-\u0026gt;irq_data.parent_data-\u0026gt;hwirq - 32; pwr_ctrl_irq_set(number, 1, 0); register_flag = 1; } } } } else { mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(keypad-\u0026gt;scan_period)); } if (wakeup_source) { if (register_flag) dev_dbg(\u0026amp;pdev-\u0026gt;dev, \u0026#34;succeed to register wakeup source!\\n\u0026#34;); else dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;failed to register wakeup source!\\n\u0026#34;); } return 0; } static int meson_gpio_kp_remove(struct platform_device *pdev) { struct gpio_keypad *keypad; keypad = platform_get_drvdata(pdev); class_unregister(\u0026amp;keypad-\u0026gt;resetkey_class); class_unregister(\u0026amp;keypad-\u0026gt;kp_class); input_unregister_device(keypad-\u0026gt;input_dev); input_free_device(keypad-\u0026gt;input_dev); del_timer(\u0026amp;keypad-\u0026gt;polling_timer); return 0; } static const struct of_device_id key_dt_match[] = { { .compatible = \u0026#34;amlogic, gpio_keypad\u0026#34;, }, {} }; static int meson_gpio_kp_suspend(struct device *dev) { struct gpio_keypad *pdata; pdata = (struct gpio_keypad *)dev_get_drvdata(dev); if (!pdata-\u0026gt;use_irq) del_timer(\u0026amp;pdata-\u0026gt;polling_timer); return 0; } static int meson_gpio_kp_resume(struct device *dev) { int i; struct gpio_keypad *pdata; pdata = (struct gpio_keypad *)dev_get_drvdata(dev); if (!pdata-\u0026gt;use_irq) mod_timer(\u0026amp;pdata-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(5)); if (get_resume_method() == POWER_KEY_WAKEUP) { for (i = 0; i \u0026lt; pdata-\u0026gt;key_size; i++) { if (pdata-\u0026gt;key[i].code == KEY_POWER) { dev_dbg(dev, \u0026#34;gpio keypad wakeup\\n\u0026#34;); input_report_key(pdata-\u0026gt;input_dev, KEY_POWER, 1); input_sync(pdata-\u0026gt;input_dev); input_report_key(pdata-\u0026gt;input_dev, KEY_POWER, 0); input_sync(pdata-\u0026gt;input_dev); break; } } } return 0; } static int meson_gpio_kp_restore(struct device *dev) { struct gpio_keypad *keypad = dev_get_drvdata(dev); struct gpio_desc *desc; int index; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) { desc = devm_gpiod_get_index(dev, \u0026#34;key\u0026#34;, index, GPIOD_IN); if (IS_ERR_OR_NULL(desc)) { dev_err(dev, \u0026#34;failed to request to gpio while restore\\n\u0026#34;); return -EINVAL; } gpiod_direction_input(desc); gpiod_set_pull(desc, GPIOD_PULL_UP); keypad-\u0026gt;key[index].desc = desc; } return meson_gpio_kp_resume(dev); } static int meson_gpio_kp_freeze(struct device *dev) { struct gpio_keypad *keypad = dev_get_drvdata(dev); int index; int ret; ret = meson_gpio_kp_suspend(dev); if (ret) return ret; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) devm_gpiod_put(dev, keypad-\u0026gt;key[index].desc); return 0; } static const struct dev_pm_ops meson_gpio_kp_pm_ops = { .suspend = meson_gpio_kp_suspend, .resume = meson_gpio_kp_resume, .freeze = meson_gpio_kp_freeze, .thaw = meson_gpio_kp_resume, .poweroff = meson_gpio_kp_suspend, .restore = meson_gpio_kp_restore, }; static struct platform_driver meson_gpio_kp_driver = { .probe = meson_gpio_kp_probe, .remove = meson_gpio_kp_remove, .driver = { .name = \u0026#34;gpio-keypad\u0026#34;, .pm = \u0026amp;meson_gpio_kp_pm_ops, .of_match_table = key_dt_match, }, }; int __init meson_gpio_kp_init(void) { return platform_driver_register(\u0026amp;meson_gpio_kp_driver); } void __exit meson_gpio_kp_exit(void) { platform_driver_unregister(\u0026amp;meson_gpio_kp_driver); } 逐函数解释 以下按代码物理顺序，逐个函数解释它干了什么。\n1. #include 头文件 — 引入外部依赖 #include \u0026lt;linux/module.h\u0026gt; // 提供 MODULE_AUTHOR、module_init() 等宏 #include \u0026lt;linux/init.h\u0026gt; // 提供 __init、__exit 宏（标记初始化/退出函数） #include \u0026lt;linux/types.h\u0026gt; // 提供 u32 类型（无符号 32 位整数） #include \u0026lt;linux/string.h\u0026gt; // 提供 strcmp()、strsep()、snprintf()、memcpy() #include \u0026lt;linux/kstrtox.h\u0026gt; // 提供 kstrtobool() —— 把字符串 \u0026#34;Y\u0026#34;/\u0026#34;N\u0026#34; 转成 bool #include \u0026lt;linux/input.h\u0026gt; // 提供 input_dev、input_event()、EV_KEY、KEY_POWER 等 #include \u0026lt;linux/platform_device.h\u0026gt; // 提供 platform_device、platform_driver 结构体 #include \u0026lt;linux/of.h\u0026gt; // 提供 of_property_read_u32() 等 DTS 解析函数 #include \u0026lt;linux/gpio/consumer.h\u0026gt;// 提供 gpiod_get_index()、gpiod_get_value()、gpiod_set_pull() #include \u0026lt;linux/interrupt.h\u0026gt; // 提供 request_irq()、irqreturn_t #include \u0026lt;linux/slab.h\u0026gt; // 提供 devm_kzalloc() —— 自动释放的内存分配 #include \u0026lt;linux/amlogic/pm.h\u0026gt; // Amlogic 私有：提供 get_resume_method() #include \u0026lt;linux/irq.h\u0026gt; // 提供 irq_to_desc() —— 通过 IRQ 号拿到 IRQ 描述符 #include \u0026lt;linux/amlogic/power_domain.h\u0026gt; // Amlogic 私有：提供 pwr_ctrl_irq_set() #include \u0026lt;linux/amlogic/gpiolib.h\u0026gt; // Amlogic 私有：提供 gpiod_set_pull() 三个带 amlogic 的头文件是 Amlogic 自己的 API，主线内核里没有。gpiod_set_pull 用于设置 GPIO 内部上拉/下拉电阻。\n2. 宏定义 — 硬编码的默认值 #define DEFAULT_SCAN_PERION\t20 // 默认轮询间隔 = 20 毫秒 #define\tDEFAULT_POLL_MODE\t0 // 默认检测模式 = polling（0=polling, 1=irq） #define KEY_JITTER_COUNT\t1 // 消抖阈值：连续 2 次（1+1）读到变化才确认 这三个值在 probe 中用作 fallback：如果 DTS 没写 scan_period 就用 20ms，没写 detect_mode 就用 polling。\n3. struct pin_desc — 描述一个按键 struct pin_desc { int current_status; // 当前电平：1=高电平(释放), 0=低电平(按下) struct gpio_desc *desc; // GPIO 描述符指针，gpiolib 用来操作这个 pin int irq_num; // 此 GPIO 对应的中断号（irq 模式下使用） u32 code; // Linux keycode，比如 KEY_POWER=116，或厂商自定义 600 u32 key_type; // 事件类型：EV_KEY（按键）或 EV_SW（开关） const char *name; // 按键名字，比如 \u0026#34;bluetooth\u0026#34;、\u0026#34;mute\u0026#34; int count; // 消抖计数器：连续检测到变化的次数 bool ignore; // 是否屏蔽此按键（true=按下也不上报事件） }; 这个结构体描述了一个按键的全部信息：\n硬件信息 → desc（接在哪个 GPIO） 输入信息 → code、key_type（上报什么事件） 运行状态 → current_status、count（消抖用） 4. struct gpio_keypad — 描述整个按键设备 struct gpio_keypad { int key_size; // 总共有几个按键（= DTS 中 key_num 的值） int use_irq; // 检测模式：0=polling, 1=irq int scan_period; // 轮询间隔（毫秒） struct pin_desc *key; // 按键数组指针，长度 = key_size struct pin_desc *current_key; // 当前正在处理的按键（消抖时指向它） struct timer_list polling_timer; // 内核定时器，polling 模式用它定时扫描 struct input_dev *input_dev; // input 设备指针，用来向上层报告事件 struct class kp_class; // 在 /sys/class/gpio_keypad/ 创建属性文件 struct class resetkey_class; // 在 /sys/class/resetkey/ 创建属性文件 unsigned int reset_count; // 蓝牙键累计按下次数（恢复出厂用） }; struct pin_desc 描述一个按键，struct gpio_keypad 描述整个设备（包含多个按键 + 运行参数 + sysfs 接口）。\n5. gpio_irq_handler() — 中断来了干什么 static irqreturn_t gpio_irq_handler(int irq, void *data) { struct gpio_keypad *keypad; keypad = (struct gpio_keypad *)data; // ① 从私有数据取出设备指针 mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, // ② 修改定时器到期时间 jiffies + msecs_to_jiffies(20)); // = 当前时间 + 20ms return IRQ_HANDLED; // ③ 告诉内核\u0026#34;中断已处理\u0026#34; } 它干了什么：当一个 GPIO 下降沿触发中断后，这个函数被调用。它只做一件事——把轮询定时器的到期时间设为\u0026quot;20ms 后\u0026quot;。定时器触发后会去读 GPIO 真实电平。\n为什么不在中断里直接读 GPIO：中断处理函数运行在中断上下文，不能调用可能睡眠的函数。gpiod_get_value() 对 I2C/SPI 扩展 GPIO 来说可能睡眠，所以推到定时器（软中断上下文）里做。此外，推到 20ms 后还顺便实现了消抖——机械按键的抖动通常在 5-20ms 内结束。\n参数说明：\nirq：触发中断的中断号（内核传入，此函数没用到） data：注册中断时传入的私有指针（就是 struct gpio_keypad *keypad） 6. report_key_code() — 向 input 子系统上报一个按键事件 static void report_key_code(struct gpio_keypad *keypad, int gpio_val) { struct pin_desc *key = keypad-\u0026gt;current_key; // ① 拿到当前要处理的按键 if (key-\u0026gt;count \u0026gt;= KEY_JITTER_COUNT) { // ② 消抖：连续 2 次变化才确认 key-\u0026gt;current_status = gpio_val; // ③ 更新记录的当前电平 if (key-\u0026gt;current_status) { // ④ 电平=1(高)，说明按键释放了 input_event(keypad-\u0026gt;input_dev, key-\u0026gt;key_type, key-\u0026gt;code, 0); // 上报事件，value=0（释放） } else { // ⑤ 电平=0(低)，说明按键按下了 input_event(keypad-\u0026gt;input_dev, key-\u0026gt;key_type, key-\u0026gt;code, 1); // 上报事件，value=1（按下） if (strcmp(key-\u0026gt;name, \u0026#34;bluetooth\u0026#34;) == 0) { // ⑥ 如果是蓝牙键 keypad-\u0026gt;reset_count++; // 累计按下次数+1 } } input_sync(keypad-\u0026gt;input_dev); // ⑦ 发送同步事件，标记一帧结束 key-\u0026gt;count = 0; // ⑧ 重置消抖计数器 } } 它干了什么：检查消抖条件是否满足（连续 KEY_JITTER_COUNT + 1 = 2 次读到变化），满足就向 input 子系统报告按键按下（value=1）或释放（value=0）。如果按键名叫 \u0026ldquo;bluetooth\u0026rdquo;，额外累加 reset_count。\ninput_event() 的参数：\nkeypad-\u0026gt;input_dev：要上报到哪个 input 设备 key-\u0026gt;key_type：事件类型（EV_KEY = 按键，EV_SW = 开关） key-\u0026gt;code：键码（比如蓝牙键是 600，mute 是 SW_MUTE_DEVICE） 0 或 1：0 = 释放，1 = 按下 input_sync() 发送 EV_SYN / SYN_REPORT，告诉用户态\u0026quot;一帧完整事件结束\u0026quot;。用户态程序（如 getevent）靠它来分组事件。\n7. polling_timer_handler() — 定时器到了干什么 static void polling_timer_handler(struct timer_list *t) { int i; int gpio_val; int is_pressing = 0; struct gpio_keypad *keypad = from_timer(keypad, t, polling_timer); // ↑ // from_timer 是个宏：已知 timer_list 成员地址 t // 反推出包含它的 struct gpio_keypad 的地址 from_timer(keypad, t, polling_timer) 展开等价于：\nkeypad = container_of(t, struct gpio_keypad, polling_timer); // 已知 t 是 \u0026amp;keypad-\u0026gt;polling_timer，反推 keypad 的地址 irq 模式分支 if (keypad-\u0026gt;use_irq) { for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { gpio_val = gpiod_get_value(keypad-\u0026gt;key[i].desc); // 读 GPIO 电平 gpio_val |= keypad-\u0026gt;key[i].ignore; // 如果被屏蔽，强制当高电平(释放) if (keypad-\u0026gt;key[i].current_status != gpio_val) { // 电平变了 → 消抖计数+1 → 尝试上报 keypad-\u0026gt;key[i].count++; keypad-\u0026gt;current_key = \u0026amp;keypad-\u0026gt;key[i]; report_key_code(keypad, gpio_val); } else { // 电平没变 → 消抖计数清零（刚才只是抖动） keypad-\u0026gt;key[i].count = 0; } // 如果有任何按键处于按下状态（读为 0 或 记录为 0） if (gpio_val == 0 || keypad-\u0026gt;key[i].current_status == 0) is_pressing = 1; } if (is_pressing) // 有按键正在按 → 重新设定定时器，继续轮询 mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(keypad-\u0026gt;scan_period)); // 如果 is_pressing == 0（所有按键都释放了），不重新设定时器 // → 定时器停止，下次按键由 GPIO 中断重新触发 polling 模式分支 } else { for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { gpio_val = gpiod_get_value(keypad-\u0026gt;key[i].desc); gpio_val |= keypad-\u0026gt;key[i].ignore; if (keypad-\u0026gt;key[i].current_status != gpio_val) { keypad-\u0026gt;key[i].count++; keypad-\u0026gt;current_key = \u0026amp;keypad-\u0026gt;key[i]; report_key_code(keypad, gpio_val); } else { keypad-\u0026gt;key[i].count = 0; } } // 无条件重新设定定时器 → 永远每 scan_period ms 触发一次 mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(keypad-\u0026gt;scan_period)); } } 两种模式的本质区别在最后几行：\nirq 模式 polling 模式 定时器何时停止 所有按键都释放后自动停止 永不停 定时器何时重启 下次 GPIO 中断触发后重启 不需要重启，一直跑 CPU 空闲功耗 零 恒定（每 20ms 执行一次） 8. table_show() — /sys/class/gpio_keypad/table 被读时干什么 static ssize_t table_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); // 已知 cls 是 keypad-\u0026gt;kp_class 的地址，反推 keypad int i; int len = 0; for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { len += sprintf(buf + len, \u0026#34;[%d]: name = %-21s status = %-5d\\n\u0026#34;, i, keypad-\u0026gt;key[i].name, // 按键名字 keypad-\u0026gt;key[i].current_status); // 当前电平 } return len; // 返回写入了多少字节 } 它干了什么：遍历所有按键，把每个按键的名字和当前电平写入 buf。用户执行 cat /sys/class/gpio_keypad/table 时会调用这个函数。\n输出示例：\n[0]: name = bluetooth status = 1 [1]: name = mute status = 1 %-21s 表示左对齐占 21 个字符宽度，%-5d 表示左对齐占 5 个数字宽度。\n9. ignore_show() — /sys/class/gpio_keypad/ignore 被读时干什么 static ssize_t ignore_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); int index; int len = 0; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) { len += sysfs_emit_at(buf, len, \u0026#34;%s,%d: %c\\n\u0026#34;, keypad-\u0026gt;key[index].name, // 按键名 keypad-\u0026gt;key[index].code, // 键码 keypad-\u0026gt;key[index].ignore ? \u0026#39;Y\u0026#39; : \u0026#39;N\u0026#39;); // 是否屏蔽 } return len; } 它干了什么：遍历所有按键，输出每个按键的\u0026quot;名字,键码: 是否屏蔽\u0026quot;。用户执行 cat /sys/class/gpio_keypad/ignore 时调用。\n输出示例：\nbluetooth,600: N mute,248: N sprintf 和 sysfs_emit_at 的区别：sysfs_emit_at 是 sprintf 的安全替代，能防止 buffer overflow，sysfs 函数中应该优先使用。\n10. ignore_store() — 向 /sys/class/gpio_keypad/ignore 写入时干什么 这是驱动中最长的函数（66 行），因为要处理用户输入的各种情况。\nstatic ssize_t ignore_store(struct class *cls, struct class_attribute *attr, const char *buf, size_t count) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); struct device *dev = keypad-\u0026gt;input_dev-\u0026gt;dev.parent; // 用来打日志 char nbuf[128]; // 栈上的临时缓冲区 char tmp[32]; // 用来拼 \u0026#34;name,code\u0026#34; 字符串 int index; char *value = nbuf; // 指向空格后面的部分（\u0026#34;Y\u0026#34; 或 \u0026#34;N\u0026#34;） char *name; // 指向空格前面的部分（\u0026#34;name,code\u0026#34; 或 \u0026#34;all\u0026#34;/\u0026#34;none\u0026#34;） bool ignore; bool found; 第 1 步：防溢出 + 去换行 if (count \u0026gt; sizeof(nbuf)) { // 防止栈溢出 dev_err(dev, \u0026#34;data is too long\\n\u0026#34;); return -EINVAL; } memcpy(nbuf, buf, count); // 拷到栈缓冲区 strreplace(nbuf, \u0026#39;\\n\u0026#39;, \u0026#39;\\0\u0026#39;); // 把换行符替换为 \u0026#39;\\0\u0026#39;（字符串结束符） 第 2 步：用空格分割输入 name = strsep(\u0026amp;value, \u0026#34; \u0026#34;); // 分割：空格前面 → name，空格后面 → value if (!name) goto err; // 没有空格 → 格式错误 strsep(\u0026amp;value, \u0026quot; \u0026quot;) 在第一个空格处切断字符串，返回空格前的部分，value 指针移动到空格后面。\n用户写入 \u0026quot;bluetooth,600 Y\\n\u0026quot; 时：\n切割前: nbuf = \u0026#34;bluetooth,600 Y\\0\u0026#34; 切割后: name → \u0026#34;bluetooth,600\u0026#34; value → \u0026#34;Y\u0026#34; 第 3 步：分支处理 if (!strcmp(\u0026#34;all\u0026#34;, name)) { // 写入 \u0026#34;all 1\u0026#34; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) keypad-\u0026gt;key[index].ignore = true; } else if (!strcmp(\u0026#34;none\u0026#34;, name)) { // 写入 \u0026#34;none 1\u0026#34; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) keypad-\u0026gt;key[index].ignore = false; } else { // 写入 \u0026#34;name,code Y\u0026#34; if (!value) goto err; // 没有 Y/N 部分 if (kstrtobool(value, \u0026amp;ignore)) { // \u0026#34;Y\u0026#34;/\u0026#34;1\u0026#34; → true, \u0026#34;N\u0026#34;/\u0026#34;0\u0026#34; → false dev_err(dev, \u0026#34;invalid \u0026#39;%s\u0026#39;, please use Y/N instead\\n\u0026#34;, value); return -EINVAL; } // 在按键数组中搜索匹配的 name,code found = false; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) { snprintf(tmp, sizeof(tmp), \u0026#34;%s,%d\u0026#34;, keypad-\u0026gt;key[index].name, keypad-\u0026gt;key[index].code); if (!strcmp(tmp, name)) { found = true; break; // 找到，跳出 } } if (!found) { dev_err(dev, \u0026#34;invalid key \u0026#39;%s\u0026#39;\\n\u0026#34;, name); return -EINVAL; } keypad-\u0026gt;key[index].ignore = ignore; // 设置屏蔽标志 } return count; // 返回 consumed 字节数 它干了什么：解析用户写入的字符串，支持三种格式：\nall 1 或 all Y → 屏蔽所有按键 none 1 或 none Y → 取消所有屏蔽 bluetooth,600 Y → 屏蔽名为 bluetooth、键码为 600 的按键 11. class 属性声明 — 把函数绑定到 sysfs 文件 static CLASS_ATTR_RO(table); // 声明 table 是只读属性 → table_show static CLASS_ATTR_RW(ignore); // 声明 ignore 是读写属性 → ignore_show + ignore_store 这组宏展开后创建了 class_attr_table 和 class_attr_ignore 两个结构体变量，内核 sysfs 框架会自动在 /sys/class/gpio_keypad/ 下创建 table 和 ignore 文件。\nstatic struct attribute *meson_gpiokey_attrs[] = { \u0026amp;class_attr_table.attr, \u0026amp;class_attr_ignore.attr, NULL // 必须以 NULL 结尾 }; ATTRIBUTE_GROUPS(meson_gpiokey); // 生成 meson_gpiokey_groups 变量 ATTRIBUTE_GROUPS(meson_gpiokey) 展开为：\nconst struct attribute_group meson_gpiokey_groups[] = { { .attrs = meson_gpiokey_attrs }, {} }; 然后在 class 注册时赋值给 kp_class.class_groups，内核自动创建对应的 sysfs 文件。\n12. count_show() — /sys/class/resetkey/count 被读时干什么 static ssize_t count_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, resetkey_class); return sprintf(buf, \u0026#34;%u\\n\u0026#34;, keypad-\u0026gt;reset_count); } static CLASS_ATTR_RO(count); static struct attribute *resetkey_attrs[] = { \u0026amp;class_attr_count.attr, NULL }; ATTRIBUTE_GROUPS(resetkey); 它干了什么：输出 reset_count 的值，即蓝牙键自驱动加载以来被按下的累计次数。用于实现\u0026quot;长按蓝牙键 N 次恢复出厂\u0026quot;。\n和 table_show 一样，这是一个 class 属性，但属于另一个 class（resetkey_class），所以在 /sys/class/resetkey/count 下暴露。\n13. meson_gpio_kp_probe() — 驱动入口，设备匹配时调用 这是驱动的核心函数（173 行）。当内核发现 DTS 中有 compatible = \u0026quot;amlogic, gpio_keypad\u0026quot; 的节点时，这个函数被调用。\n第 1 步：分配 per-device 数据 keypad = devm_kzalloc(\u0026amp;pdev-\u0026gt;dev, sizeof(struct gpio_keypad), GFP_KERNEL); devm_kzalloc 干了三件事：\n分配 sizeof(struct gpio_keypad) 字节内存 内存清零（kz = kernel zero） 把内存\u0026quot;挂\u0026quot;到 pdev-\u0026gt;dev 上（devm = device managed），设备移除时自动释放 第 2 步：从 DTS 读取参数 ret = of_property_read_u32(pdev-\u0026gt;dev.of_node, // DTS 节点 \u0026#34;detect_mode\u0026#34;, // 属性名 \u0026amp;keypad-\u0026gt;use_irq); // 读到这个变量 if (ret) // 返回负值 = 属性不存在 keypad-\u0026gt;use_irq = DEFAULT_POLL_MODE; // 用默认值 0 (polling) of_property_read_u32 从 DTS 节点中读取一个 u32 属性。如果属性不存在（比如 DTS 里没写 detect_mode），返回负值，驱动用默认值。\n同样方式读取 scan_period（默认 20ms）：\nret = of_property_read_u32(pdev-\u0026gt;dev.of_node, \u0026#34;scan_period\u0026#34;, \u0026amp;keypad-\u0026gt;scan_period); if (ret) keypad-\u0026gt;scan_period = DEFAULT_SCAN_PERION; 读取布尔属性（存在即 true）：\nif (of_property_read_bool(pdev-\u0026gt;dev.of_node, \u0026#34;wakeup-source\u0026#34;)) wakeup_source = 1; // DTS 中有 wakeup-source 属性 读取必需属性（不存在就报错返回）：\nret = of_property_read_u32(pdev-\u0026gt;dev.of_node, \u0026#34;key_num\u0026#34;, \u0026amp;keypad-\u0026gt;key_size); if (ret) { dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;failed to get key_num!\\n\u0026#34;); return -EINVAL; // 返回错误，probe 失败 } 第 3 步：分配按键数组 keypad-\u0026gt;key = devm_kzalloc(\u0026amp;pdev-\u0026gt;dev, (keypad-\u0026gt;key_size) * sizeof(*keypad-\u0026gt;key), GFP_KERNEL); 根据 key_num 的值，分配能容纳所有按键的 pin_desc 数组。\n第 4 步：逐按键获取 GPIO 和解析属性 for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { desc = devm_gpiod_get_index(\u0026amp;pdev-\u0026gt;dev, \u0026#34;key\u0026#34;, i, GPIOD_IN); devm_gpiod_get_index(dev, \u0026quot;key\u0026quot;, i, GPIOD_IN) 干了什么：\n在 DTS 中查找 key-gpios 属性（内核自动在 \u0026quot;key\u0026quot; 后追加 -gpios 后缀） 取该属性的第 i 个条目 返回 GPIO 描述符，并设置为输入模式（GPIOD_IN） 对应 DTS：\nkey-gpios = \u0026lt;\u0026amp;gpio GPIOD_2 GPIO_ACTIVE_HIGH ← i=0 \u0026amp;gpio GPIOD_3 GPIO_ACTIVE_HIGH\u0026gt;; ← i=1 然后逐个解析数组属性：\nkeypad-\u0026gt;key[i].desc = desc; // 保存 GPIO 描述符 keypad-\u0026gt;key[i].current_status = 1; // 初始状态 = 高电平(释放) // 读取 key_code 数组的第 i 个值 ret = of_property_read_u32_index(pdev-\u0026gt;dev.of_node, \u0026#34;key_code\u0026#34;, i, \u0026amp;keypad-\u0026gt;key[i].code); // 读取 key_type 数组的第 i 个值（可选，默认 EV_KEY） ret = of_property_read_u32_index(pdev-\u0026gt;dev.of_node, \u0026#34;key_type\u0026#34;, i, \u0026amp;keypad-\u0026gt;key[i].key_type); if (ret) keypad-\u0026gt;key[i].key_type = EV_KEY; // 读取 key_name 数组的第 i 个值（字符串数组） ret = of_property_read_string_index(pdev-\u0026gt;dev.of_node, \u0026#34;key_name\u0026#34;, i, \u0026amp;keypad-\u0026gt;key[i].name); DTS 中的数组属性：\nkey_code = \u0026lt;600 SW_MUTE_DEVICE\u0026gt;; ← index=0 是 600, index=1 是 SW_MUTE_DEVICE key_type = \u0026lt;EV_KEY EV_SW\u0026gt;; ← index=0 是 EV_KEY, index=1 是 EV_SW key_name = \u0026#34;bluetooth\u0026#34;, \u0026#34;mute\u0026#34;; ← index=0 是 \u0026#34;bluetooth\u0026#34;, index=1 是 \u0026#34;mute\u0026#34; 最后配置 GPIO 硬件：\ngpiod_direction_input(keypad-\u0026gt;key[i].desc); // 设为输入模式 gpiod_set_pull(keypad-\u0026gt;key[i].desc, GPIOD_PULL_UP);// 使能内部上拉电阻 } gpiod_set_pull(GPIOD_PULL_UP) 使能芯片内部的上拉电阻，确保按键未按下时 GPIO 读到的是确定的高电平（而不是悬空）。\n第 5 步：注册 sysfs class 接口 keypad-\u0026gt;kp_class.name = \u0026#34;gpio_keypad\u0026#34;; // class 名称 → /sys/class/gpio_keypad/ keypad-\u0026gt;kp_class.owner = THIS_MODULE; keypad-\u0026gt;kp_class.class_groups = meson_gpiokey_groups; // 绑定的属性组 (table + ignore) ret = class_register(\u0026amp;keypad-\u0026gt;kp_class); // 注册 → sysfs 文件自动创建 keypad-\u0026gt;resetkey_class.name = \u0026#34;resetkey\u0026#34;; // class 名称 → /sys/class/resetkey/ keypad-\u0026gt;resetkey_class.owner = THIS_MODULE; keypad-\u0026gt;resetkey_class.class_groups = resetkey_groups;// 绑定的属性组 (count) keypad-\u0026gt;reset_count = 0; ret = class_register(\u0026amp;keypad-\u0026gt;resetkey_class); if (ret) { class_unregister(\u0026amp;keypad-\u0026gt;kp_class); // 失败时要注销已注册的 class return -EINVAL; } class_register 会触发内核在 /sys/class/ 下创建对应目录，并根据 class_groups 自动创建属性文件。\n第 6 步：注册 input 设备 input_dev = input_allocate_device(); // 分配 input_dev 结构体 input_allocate_device 分配一个新的 input_dev，后续往里填属性。\nfor (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { input_set_capability(input_dev, keypad-\u0026gt;key[i].key_type, keypad-\u0026gt;key[i].code); } input_set_capability 告诉 input 子系统\u0026quot;这个设备能产生这种事件\u0026quot;。上层程序（如 Android InputReader）通过查询 capability 来判断设备用途。比如声明了 EV_KEY / KEY_VOLUMEUP，系统就知道用这个设备来控制音量。\ninput_dev-\u0026gt;name = \u0026#34;gpio_keypad\u0026#34;; // 设备名，getevent -l 会显示 input_dev-\u0026gt;phys = \u0026#34;gpio_keypad/input0\u0026#34;; // 物理路径标识 input_dev-\u0026gt;dev.parent = \u0026amp;pdev-\u0026gt;dev; // 父设备（建立 sysfs 层级关系） input_dev-\u0026gt;id.bustype = BUS_ISA; input_dev-\u0026gt;id.vendor = 0x0001; input_dev-\u0026gt;id.product = 0x0001; input_dev-\u0026gt;id.version = 0x0100; input_dev-\u0026gt;rep[REP_DELAY] = 0xffffffff; // 禁用自动按键重复 input_dev-\u0026gt;rep[REP_PERIOD] = 0xffffffff; // （驱动自己处理） rep[REP_DELAY] = 0xffffffff：设为最大值，等于禁用 input 子系统的自动长按重复功能。驱动自己通过消抖逻辑处理按键状态，不需要 input 核来产生额外的重复事件。\nkeypad-\u0026gt;input_dev = input_dev; // 保存指针 ret = input_register_device(keypad-\u0026gt;input_dev); // 注册 → 创建 /dev/input/eventX if (ret \u0026lt; 0) { input_free_device(keypad-\u0026gt;input_dev); // 注册失败要释放 return -EINVAL; } input_register_device 在内核中注册设备，用户态会出现 /dev/input/eventX。\n第 7 步：绑定数据 + 启动检测 platform_set_drvdata(pdev, keypad); // 把 keypad 存到 pdev 里 platform_set_drvdata 把 per-device 数据存到 platform_device 的私有字段里。后续 remove、suspend、resume 中通过 platform_get_drvdata(pdev) / dev_get_drvdata(dev) 取出。\ntimer_setup(\u0026amp;keypad-\u0026gt;polling_timer, // 初始化定时器 polling_timer_handler, // 回调函数 0); // flags timer_setup 初始化定时器，但不启动。定时器到期时调用 polling_timer_handler。\n第 8 步：根据模式启动检测 if (keypad-\u0026gt;use_irq) { // ===== 中断模式 ===== for (i = 0; i \u0026lt; keypad-\u0026gt;key_size; i++) { keypad-\u0026gt;key[i].count = 0; keypad-\u0026gt;key[i].irq_num = gpiod_to_irq(keypad-\u0026gt;key[i].desc); // ↑ GPIO 描述符 → IRQ 中断号 ret = devm_request_irq(\u0026amp;pdev-\u0026gt;dev, keypad-\u0026gt;key[i].irq_num, // 中断号 gpio_irq_handler, // 处理函数 IRQF_TRIGGER_FALLING, // 下降沿触发 \u0026#34;gpio_keypad\u0026#34;, // 名称（/proc/interrupts 中可见） keypad); // 传给 handler 的 data // ... gpiod_to_irq 查询 GPIO 对应的中断号。devm_request_irq 注册中断处理函数——当该 GPIO 出现下降沿（高→低）时，内核调用 gpio_irq_handler。\nIRQF_TRIGGER_FALLING：因为空闲时上拉到高电平，按下时拉低，下降沿就是按键按下的瞬间。\n对 KEY_POWER 的特殊处理：\nif (keypad-\u0026gt;key[i].code == KEY_POWER) { d = irq_to_desc(keypad-\u0026gt;key[i].irq_num); // 拿到 IRQ 描述符 if (d) { number = d-\u0026gt;irq_data.parent_data-\u0026gt;hwirq - 32; // 算出硬件 IRQ 编号 pwr_ctrl_irq_set(number, 1, 0); // Amlogic 私有：设为唤醒源 register_flag = 1; } } pwr_ctrl_irq_set 是 Amlogic 的 API，把这个 GPIO 中断注册为唤醒源——即使系统休眠了，按下电源键仍然能唤醒。\n} else { // ===== polling 模式 ===== mod_timer(\u0026amp;keypad-\u0026gt;polling_timer, jiffies + msecs_to_jiffies(keypad-\u0026gt;scan_period)); // ↑ 当前时间 + scan_period ms 后触发定时器 } mod_timer 修改定时器的到期时间。第一次调用等于\u0026quot;在 scan_period ms 后触发\u0026quot;。定时器触发后调用 polling_timer_handler，在那个函数里会再次调用 mod_timer 实现循环。\njiffies 是内核维护的全局变量，记录自系统启动以来的时钟节拍数。msecs_to_jiffies(20) 把 20ms 转换成对应的节拍数。\n第 9 步：唤醒源状态日志 if (wakeup_source) { if (register_flag) dev_dbg(\u0026amp;pdev-\u0026gt;dev, \u0026#34;succeed to register wakeup source!\\n\u0026#34;); else dev_err(\u0026amp;pdev-\u0026gt;dev, \u0026#34;failed to register wakeup source!\\n\u0026#34;); } return 0; // probe 成功 } 14. meson_gpio_kp_remove() — 设备被移除时调用 static int meson_gpio_kp_remove(struct platform_device *pdev) { struct gpio_keypad *keypad; keypad = platform_get_drvdata(pdev); // 拿出 probe 时存的数据 class_unregister(\u0026amp;keypad-\u0026gt;resetkey_class); // ① 注销 /sys/class/resetkey/ class_unregister(\u0026amp;keypad-\u0026gt;kp_class); // ② 注销 /sys/class/gpio_keypad/ input_unregister_device(keypad-\u0026gt;input_dev); // ③ 注销 /dev/input/eventX input_free_device(keypad-\u0026gt;input_dev); // ④ 释放 input_dev 内存 del_timer(\u0026amp;keypad-\u0026gt;polling_timer); // ⑤ 删除定时器（防止之后触发） return 0; } 它干了什么：按与 probe 相反的注册顺序，依次注销 class、注销 input 设备、释放内存、删除定时器。\ndel_timer 为什么放最后：del_timer 是同步的——如果定时器正在执行，会等它执行完。这时候 input_dev 还有效，定时器 handler 如果正好在跑，访问 input_dev 也不会崩。\ndevm_kzalloc 分配的内存不需要在这里 kfree——设备移除时内核自动释放。\n15. of_device_id — DTS 匹配表 static const struct of_device_id key_dt_match[] = { { .compatible = \u0026#34;amlogic, gpio_keypad\u0026#34;, }, // 匹配 DTS 中 compatible = \u0026#34;amlogic, gpio_keypad\u0026#34; {} // 必须以空条目结尾 }; 它干了什么：告诉内核\u0026quot;我的驱动负责处理 compatible 为 \u0026quot;amlogic, gpio_keypad\u0026quot; 的设备\u0026quot;。当内核解析 DTS 遇到匹配节点时，调用 probe。\n16. 电源管理回调 — 休眠/唤醒时干什么 suspend — 系统要休眠了 static int meson_gpio_kp_suspend(struct device *dev) { struct gpio_keypad *pdata; pdata = (struct gpio_keypad *)dev_get_drvdata(dev); // 拿到 per-device 数据 if (!pdata-\u0026gt;use_irq) del_timer(\u0026amp;pdata-\u0026gt;polling_timer); // polling 模式：停定时器省电 // irq 模式：不动，GPIO 中断本身就是唤醒源 return 0; } resume — 系统从休眠恢复了 static int meson_gpio_kp_resume(struct device *dev) { int i; struct gpio_keypad *pdata; pdata = (struct gpio_keypad *)dev_get_drvdata(dev); if (!pdata-\u0026gt;use_irq) mod_timer(\u0026amp;pdata-\u0026gt;polling_timer, // polling：重新启动定时器 jiffies + msecs_to_jiffies(5)); // 5ms 后开始（比正常更快） if (get_resume_method() == POWER_KEY_WAKEUP) { // 如果是电源键唤醒的 for (i = 0; i \u0026lt; pdata-\u0026gt;key_size; i++) { if (pdata-\u0026gt;key[i].code == KEY_POWER) { // 模拟一次电源键按下+释放，通知上层\u0026#34;用户按了电源键\u0026#34; input_report_key(pdata-\u0026gt;input_dev, KEY_POWER, 1); input_sync(pdata-\u0026gt;input_dev); input_report_key(pdata-\u0026gt;input_dev, KEY_POWER, 0); input_sync(pdata-\u0026gt;input_dev); break; } } } return 0; } 它干了什么：\npolling 模式：重新启动定时器（5ms 后第一次触发，比正常 20ms 更快，确保快速感知按键） 如果系统是被电源键唤醒的，向 input 子系统上报一次电源键事件。这样 Android 框架层能识别\u0026quot;用户按了电源键唤醒\u0026quot;，正确处理亮屏逻辑。 freeze — S4 休眠（Suspend-to-Disk）冻结 static int meson_gpio_kp_freeze(struct device *dev) { struct gpio_keypad *keypad = dev_get_drvdata(dev); int index; int ret; ret = meson_gpio_kp_suspend(dev); // 先执行 suspend：停定时器 if (ret) return ret; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) devm_gpiod_put(dev, keypad-\u0026gt;key[index].desc); // 释放所有 GPIO 描述符 return 0; } 它干了什么：停定时器 + 释放 GPIO 描述符。S4 休眠时内核镜像会完全重建，GPIO 描述符（内核地址）不再有效，必须释放。\nrestore — S4 休眠恢复 static int meson_gpio_kp_restore(struct device *dev) { struct gpio_keypad *keypad = dev_get_drvdata(dev); struct gpio_desc *desc; int index; for (index = 0; index \u0026lt; keypad-\u0026gt;key_size; index++) { desc = devm_gpiod_get_index(dev, \u0026#34;key\u0026#34;, index, GPIOD_IN); // 重新获取 GPIO if (IS_ERR_OR_NULL(desc)) return -EINVAL; gpiod_direction_input(desc); // 重新设为输入 gpiod_set_pull(desc, GPIOD_PULL_UP); // 重新使能上拉 keypad-\u0026gt;key[index].desc = desc; // 更新描述符指针 } return meson_gpio_kp_resume(dev); // 复用 resume 逻辑 } 它干了什么：S4 恢复后，所有 GPIO 描述符都失效了，需要重新获取。获取后重新配置为输入+上拉。最后调用 resume 启动检测。\nPM ops 汇总 static const struct dev_pm_ops meson_gpio_kp_pm_ops = { .suspend = meson_gpio_kp_suspend, // S3 休眠 → 停定时器 .resume = meson_gpio_kp_resume, // S3 恢复 → 启定时器 + 模拟电源键 .freeze = meson_gpio_kp_freeze, // S4 冻结 → 停定时器 + 释放 GPIO .thaw = meson_gpio_kp_resume, // S4 解冻 → 复用 resume .poweroff = meson_gpio_kp_suspend, // 关机 → 复用 suspend .restore = meson_gpio_kp_restore, // S4 恢复 → 重新获取 GPIO + resume }; thaw 是 freeze 的反操作，这里直接复用了 resume 的逻辑（重新启动定时器 + 模拟电源键）。poweroff 是关机的回调，直接复用 suspend（停定时器）。\n17. platform_driver 结构体 — 把驱动各部件组装起来 static struct platform_driver meson_gpio_kp_driver = { .probe = meson_gpio_kp_probe, // 设备匹配时调用 .remove = meson_gpio_kp_remove, // 设备移除时调用 .driver = { .name = \u0026#34;gpio-keypad\u0026#34;, // 驱动名称 .pm = \u0026amp;meson_gpio_kp_pm_ops, // 电源管理回调 .of_match_table = key_dt_match, // DTS compatible 匹配表 }, }; 它干了什么：把前面写的所有函数（probe、remove、PM 回调、匹配表）组装成一个 platform_driver。这是一个描述\u0026quot;这个驱动能干吗\u0026quot;的汇总结构体。\n18. module_init 和 module_exit — 驱动的入口和出口 int __init meson_gpio_kp_init(void) { return platform_driver_register(\u0026amp;meson_gpio_kp_driver); // ↑ 告诉内核：\u0026#34;我准备好了，有匹配的设备就叫我\u0026#34; } void __exit meson_gpio_kp_exit(void) { platform_driver_unregister(\u0026amp;meson_gpio_kp_driver); // ↑ 告诉内核：\u0026#34;我要退出了，之前绑定的设备请解绑\u0026#34; } platform_driver_register 干了什么：把 meson_gpio_kp_driver 注册到内核的 platform 总线。内核遍历已解析的 DTS 节点，如果 compatible 匹配就调用 probe。\n__init 宏：告诉编译器\u0026quot;这个函数只在初始化阶段使用\u0026quot;，初始化完成后内核可回收该代码占用的内存。\n函数调用关系总图 platform_driver_register() └─ .probe = meson_gpio_kp_probe() ├─ devm_kzalloc() ← 分配 per-device 数据 ├─ of_property_read_u32() × N ← 从 DTS 读参数 ├─ devm_kzalloc() ← 分配按键数组 ├─ for each key: │ ├─ devm_gpiod_get_index() ← 获取 GPIO │ ├─ of_property_read_u32_index() × 2 ← 读 key_code, key_type │ ├─ of_property_read_string_index() ← 读 key_name │ ├─ gpiod_direction_input() ← 设为输入 │ └─ gpiod_set_pull(PULL_UP) ← 使能上拉 ├─ class_register() × 2 ← 创建 /sys/class/... 接口 ├─ input_allocate_device() ← 分配 input 设备 ├─ input_set_capability() × N ← 声明设备能力 ├─ input_register_device() ← 注册 input → /dev/input/eventX ├─ platform_set_drvdata() ← 绑定数据 ├─ timer_setup() ← 初始化定时器 └─ if irq: ├─ gpiod_to_irq() × N ← GPIO → IRQ 号 └─ devm_request_irq() × N ← 注册中断 else: └─ mod_timer() ← 启动轮询定时器 运行时: 中断触发 → gpio_irq_handler() ← 中断上下文 └─ mod_timer(20ms 后) └─ polling_timer_handler() ← 软中断上下文 ├─ gpiod_get_value() × N ← 读 GPIO 电平 └─ report_key_code() ← 消抖 + 上报 ├─ input_event() ← 向 input 上报 └─ input_sync() ← 同步帧 sysfs 访问: cat /sys/class/gpio_keypad/table → table_show() cat /sys/class/gpio_keypad/ignore → ignore_show() echo \u0026#34;xxx\u0026#34; \u0026gt; /sys/class/gpio_keypad/ignore → ignore_store() cat /sys/class/resetkey/count → count_show() 休眠/恢复: suspend → 停轮询定时器 resume → 启轮询定时器 + (如果是电源键唤醒)上报电源键事件 freeze → 停轮询定时器 + 释放所有 GPIO 描述符 restore → 重新获取所有 GPIO + resume 驱动卸载: platform_driver_unregister() └─ .remove = meson_gpio_kp_remove() ├─ class_unregister() × 2 ├─ input_unregister_device() ├─ input_free_device() └─ del_timer() 驱动编写常用模式速查 读完这份驱动，你遇到了以下模式——在新驱动中会反复出现：\n模式 函数/宏 在哪里看到 自动内存管理 devm_kzalloc() probe 第 1 步 DTS 属性解析 of_property_read_u32() / _index() probe 第 2、4 步 GPIO consumer API devm_gpiod_get_index() probe 第 4 步 容器反查 container_of() / from_timer() table_show, polling_timer_handler sysfs 属性暴露 CLASS_ATTR_RO / CLASS_ATTR_RW 代码 §11 input 设备注册 input_allocate_device() + input_register_device() probe 第 6 步 定时器 timer_setup() + mod_timer() probe 第 7 步 中断注册 devm_request_irq() probe 第 8 步 per-device 数据绑定 platform_set_drvdata() / _get_drvdata() probe 第 7 步 电源管理 dev_pm_ops 结构体 代码 §16 platform 驱动模板 platform_driver 结构体 代码 §17 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-6-%E6%80%BB%E7%BB%93%E4%B8%8E%E6%89%A9%E5%B1%95/","summary":"\u003ch1 id=\"level-6-总结与扩展逐函数拆解-gpio-按键驱动\"\u003eLevel 6 总结与扩展：逐函数拆解 GPIO 按键驱动\u003c/h1\u003e\n\u003cp\u003e本文是 \u003ca href=\"level-6-%E6%8C%89%E9%94%AE.md\"\u003elevel-6-按键.md\u003c/a\u003e 的延伸，以 Amlogic \u003ccode\u003egpio_keypad.c\u003c/code\u003e（557 行）为样本，\u003cstrong\u003e按照代码的物理顺序，逐函数解释它干了什么\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e读完本文你会：能自己读懂一个陌生驱动，知道每个函数在做什么。\u003c/p\u003e\n\u003cp\u003e驱动源文件：\u003ccode\u003ecommon/common_drivers/drivers/input/keyboard/gpio_keypad.c\u003c/code\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"代码总览完整源码\"\u003e代码总览（完整源码）\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e以下是 \u003ccode\u003egpio_keypad.c\u003c/code\u003e 的完整代码。每个函数后面跟着逐行解释。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// SPDX-License-Identifier: (GPL-2.0+ OR MIT)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e/*\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e * Copyright (c) 2019 Amlogic, Inc. All rights reserved.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/module.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/init.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/types.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/string.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/kstrtox.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/input.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/platform_device.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/of.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/gpio/consumer.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/interrupt.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/slab.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/amlogic/pm.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/irq.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/amlogic/power_domain.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;linux/amlogic/gpiolib.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#define DEFAULT_SCAN_PERION\t20\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#define\tDEFAULT_POLL_MODE\t0\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#define KEY_JITTER_COUNT\t1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e pin_desc {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e current_status;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edesc;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e irq_num;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tu32 code;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tu32 key_type;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ename;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e count;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e ignore;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e key_size;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e use_irq;\u003cspan style=\"color:#75715e\"\u003e/* 1:irq mode ; 0:polling mode */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e scan_period;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e pin_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekey;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e pin_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ecurrent_key;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e timer_list polling_timer;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e input_dev \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003einput_dev;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class kp_class;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class resetkey_class;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e reset_count;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eirqreturn_t\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003egpio_irq_handler\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e irq, \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edata)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e)data;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003emod_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t  jiffies \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsecs_to_jiffies\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e20\u003c/span\u003e));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e IRQ_HANDLED;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ereport_key_code\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e gpio_val)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e pin_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekey \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecurrent_key;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecount \u003cspan style=\"color:#f92672\"\u003e\u0026gt;=\u003c/span\u003e KEY_JITTER_COUNT) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkey\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecurrent_status \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e gpio_val;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecurrent_status) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_event\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev, key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_type,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t    key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecode, \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key %d up.\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecode);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_event\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev, key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_type,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t    key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecode, \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#75715e\"\u003e// Count key press\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003estrcmp\u003c/span\u003e(key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ename, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;bluetooth\u0026#34;\u003c/span\u003e) \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ereset_count\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key %d down.\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, key\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecode);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_sync\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkey\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecount \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003epolling_timer_handler\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e timer_list \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003et)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e i;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e gpio_val;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e is_pressing \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efrom_timer\u003c/span\u003e(keypad, t, polling_timer);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003euse_irq) {\u003cspan style=\"color:#75715e\"\u003e/* irq mode */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tgpio_val \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003egpiod_get_value\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].desc);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tgpio_val \u003cspan style=\"color:#f92672\"\u003e|=\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].ignore;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].current_status \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e gpio_val) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].count\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecurrent_key \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003ereport_key_code\u003c/span\u003e(keypad, gpio_val);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].count \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (gpio_val \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e||\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].current_status \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tis_pressing \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (is_pressing)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003emod_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t  jiffies \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t  \u003cspan style=\"color:#a6e22e\"\u003emsecs_to_jiffies\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003escan_period));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\u003cspan style=\"color:#75715e\"\u003e/* polling mode */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tgpio_val \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003egpiod_get_value\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].desc);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tgpio_val \u003cspan style=\"color:#f92672\"\u003e|=\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].ignore;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].current_status \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e gpio_val) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].count\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ecurrent_key \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003ereport_key_code\u003c/span\u003e(keypad, gpio_val);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].count \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003emod_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t  jiffies \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsecs_to_jiffies\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003escan_period));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003essize_t\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etable_show\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ecls, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class_attribute \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eattr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t  \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ebuf)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003econtainer_of\u003c/span\u003e(cls,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad, kp_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e i;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e len \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlen \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esprintf\u003c/span\u003e(buf \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e len,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;[%d]: name = %-21s status = %-5d\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, i,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].name,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].current_status);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e len;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003essize_t\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eignore_show\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ecls, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class_attribute \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eattr, \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ebuf)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003econtainer_of\u003c/span\u003e(cls, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad, kp_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e index;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e len \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (index \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; index \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; index\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlen \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esysfs_emit_at\u003c/span\u003e(buf, len, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;%s,%d: %c\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t     keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].name,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t     keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].code,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t     keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].ignore \u003cspan style=\"color:#f92672\"\u003e?\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Y\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;N\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e len;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003essize_t\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eignore_store\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ecls, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class_attribute \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eattr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ebuf, \u003cspan style=\"color:#66d9ef\"\u003esize_t\u003c/span\u003e count)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003econtainer_of\u003c/span\u003e(cls, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad, kp_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.parent;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e nbuf[\u003cspan style=\"color:#ae81ff\"\u003e128\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e tmp[\u003cspan style=\"color:#ae81ff\"\u003e32\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e index;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003evalue \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nbuf;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ename;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e ignore;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e found;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (count \u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003esizeof\u003c/span\u003e(nbuf)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;data is too long\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003ememcpy\u003c/span\u003e(nbuf, buf, count);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003estrreplace\u003c/span\u003e(nbuf, \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\\n\u0026#39;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\\0\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tname \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrsep\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003evalue, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34; \u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003ename)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003egoto\u003c/span\u003e err;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003estrcmp\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;all\u0026#34;\u003c/span\u003e, name)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (index \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; index \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; index\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].ignore \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e true;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;ignore all keys\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003estrcmp\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;none\u0026#34;\u003c/span\u003e, name)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (index \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; index \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; index\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].ignore \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e false;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;enable all keys\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003evalue)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003egoto\u003c/span\u003e err;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003ekstrtobool\u003c/span\u003e(value, \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003eignore)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;invalid \u0026#39;%s\u0026#39;, please use Y/N instead\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, value);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#75715e\"\u003e/* search key */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tfound \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e false;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (index \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; index \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; index\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003esnprintf\u003c/span\u003e(tmp, \u003cspan style=\"color:#66d9ef\"\u003esizeof\u003c/span\u003e(tmp), \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;%s,%d\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].name,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].code);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003estrcmp\u003c/span\u003e(tmp, name)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\tfound \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e true;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ebreak\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003efound) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;invalid key \u0026#39;%s\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, name);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].ignore \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e ignore;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;ignore %s: %s\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, name, value);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e count;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eerr:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;the correct format is: [name],[code] [ignore]\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eCLASS_ATTR_RO\u003c/span\u003e(table);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eCLASS_ATTR_RW\u003c/span\u003e(ignore);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e attribute \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003emeson_gpiokey_attrs[] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003eclass_attr_table.attr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003eclass_attr_ignore.attr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tNULL\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eATTRIBUTE_GROUPS\u003c/span\u003e(meson_gpiokey);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// Add show function for reset count\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003essize_t\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ecount_show\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ecls, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e class_attribute \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eattr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                         \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ebuf)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003econtainer_of\u003c/span\u003e(cls,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad, resetkey_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esprintf\u003c/span\u003e(buf, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;%u\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ereset_count);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eCLASS_ATTR_RO\u003c/span\u003e(count);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e attribute \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eresetkey_attrs[] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003eclass_attr_count.attr,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    NULL\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eATTRIBUTE_GROUPS\u003c/span\u003e(resetkey);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_probe\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e platform_device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003epdev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edesc;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e ret, i;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e input_dev \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003einput_dev;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e irq_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ed;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e number;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e wakeup_source \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e register_flag \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;pdev-\u0026gt;dev.of_node == NULL!\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edevm_kzalloc\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t      \u003cspan style=\"color:#66d9ef\"\u003esizeof\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad), GFP_KERNEL);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003ekeypad)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eof_property_read_u32\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t   \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;detect_mode\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003euse_irq);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#75715e\"\u003e/* The default mode is polling. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003euse_irq \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e DEFAULT_POLL_MODE;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eof_property_read_u32\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t   \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;scan_period\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003escan_period);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#75715e\"\u003e/* he default scan period is 20. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003escan_period \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e DEFAULT_SCAN_PERION;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eof_property_read_bool\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;wakeup-source\u0026#34;\u003c/span\u003e))\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\twakeup_source \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eof_property_read_u32\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t   \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key_num\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;failed to get key_num!\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edevm_kzalloc\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t   (keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size) \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003esizeof\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t   GFP_KERNEL);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey))\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#75715e\"\u003e/* get all gpio desc. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tdesc \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edevm_gpiod_get_index\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key\u0026#34;\u003c/span\u003e, i, GPIOD_IN);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eIS_ERR_OR_NULL\u003c/span\u003e(desc))\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].desc \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e desc;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#75715e\"\u003e/* The gpio default is high level. */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].current_status \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eof_property_read_u32_index\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key_code\u0026#34;\u003c/span\u003e, i,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].code);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;find key_code=%d finished\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, i);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eof_property_read_u32_index\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key_type\u0026#34;\u003c/span\u003e, i,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].key_type);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].key_type \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e EV_KEY;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eof_property_read_string_index\u003c/span\u003e(pdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.of_node,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key_name\u0026#34;\u003c/span\u003e, i,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t    \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].name);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;find key_name=%d finished\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, i);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003egpiod_direction_input\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].desc);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003egpiod_set_pull\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].desc, GPIOD_PULL_UP);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekp_class.name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gpio_keypad\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekp_class.owner \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e THIS_MODULE;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekp_class.class_groups \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpiokey_groups;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass_register\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekp_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;fail to create gpio keypad class.\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#75715e\"\u003e// After registering kp_class, add resetkey class registration\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eresetkey_class.name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;resetkey\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eresetkey_class.owner \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e THIS_MODULE;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eresetkey_class.class_groups \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e resetkey_groups;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ereset_count \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass_register\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eresetkey_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;fail to create resetkey class.\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003eclass_unregister\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekp_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#75715e\"\u003e/* input */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003einput_allocate_device\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003einput_dev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_set_capability\u003c/span\u003e(input_dev, keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].key_type,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t     keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].code);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;%s key(%d) type(0x%x) registered.\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].name, keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].code,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].key_type);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ename \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gpio_keypad\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ephys \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gpio_keypad/input0\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev.parent \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eid.bustype \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e BUS_ISA;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eid.vendor \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0x0001\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eid.product \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0x0001\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eid.version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0x0100\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003erep[REP_DELAY] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0xffffffff\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003erep[REP_PERIOD] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0xffffffff\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekeycodesize \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003esizeof\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eshort\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tinput_dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekeycodemax \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0x1ff\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e input_dev;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003einput_register_device\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_free_device\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003eplatform_set_drvdata\u003c/span\u003e(pdev, keypad);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003etimer_setup\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t    polling_timer_handler, \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003euse_irq) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].count \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].irq_num \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003egpiod_to_irq\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].desc);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edevm_request_irq\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t       keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].irq_num,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t       gpio_irq_handler,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t       IRQF_TRIGGER_FALLING,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t       \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gpio_keypad\u0026#34;\u003c/span\u003e, keypad);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Requesting irq failed!\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_free_device\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edel_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].code \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e KEY_POWER) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\td \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eirq_to_desc\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].irq_num);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (d) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\tnumber \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t d\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eirq_data.parent_data\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ehwirq \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e32\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003epwr_ctrl_irq_set\u003c/span\u003e(number, \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\tregister_flag \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t} \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003emod_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t  jiffies \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsecs_to_jiffies\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003escan_period));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (wakeup_source) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (register_flag)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;succeed to register wakeup source!\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;failed to register wakeup source!\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_remove\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e platform_device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003epdev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tkeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eplatform_get_drvdata\u003c/span\u003e(pdev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003eclass_unregister\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eresetkey_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003eclass_unregister\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekp_class);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003einput_unregister_device\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003einput_free_device\u003c/span\u003e(keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003edel_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ekeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e of_device_id key_dt_match[] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t{ .compatible \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;amlogic, gpio_keypad\u0026#34;\u003c/span\u003e, },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t{}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_suspend\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003epdata;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tpdata \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e)\u003cspan style=\"color:#a6e22e\"\u003edev_get_drvdata\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003epdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003euse_irq)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edel_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_resume\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e i;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003epdata;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tpdata \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e)\u003cspan style=\"color:#a6e22e\"\u003edev_get_drvdata\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003epdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003euse_irq)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003emod_timer\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003epdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003epolling_timer,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t  jiffies \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsecs_to_jiffies\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e5\u003c/span\u003e));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eget_resume_method\u003c/span\u003e() \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e POWER_KEY_WAKEUP) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e pdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (pdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[i].code \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e KEY_POWER) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_dbg\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gpio keypad wakeup\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_report_key\u003c/span\u003e(pdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t KEY_POWER,  \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_sync\u003c/span\u003e(pdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_report_key\u003c/span\u003e(pdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\t\t KEY_POWER,  \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#a6e22e\"\u003einput_sync\u003c/span\u003e(pdata\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003einput_dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ebreak\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_restore\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edev_get_drvdata\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_desc \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edesc;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e index;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (index \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; index \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; index\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tdesc \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edevm_gpiod_get_index\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;key\u0026#34;\u003c/span\u003e, index, GPIOD_IN);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eIS_ERR_OR_NULL\u003c/span\u003e(desc)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#a6e22e\"\u003edev_err\u003c/span\u003e(dev, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;failed to request to gpio while restore\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003egpiod_direction_input\u003c/span\u003e(desc);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003egpiod_set_pull\u003c/span\u003e(desc, GPIOD_PULL_UP);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tkeypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].desc \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e desc;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_resume\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_freeze\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e gpio_keypad \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ekeypad \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edev_get_drvdata\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e index;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e ret;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_suspend\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e ret;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (index \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; index \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey_size; index\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#a6e22e\"\u003edevm_gpiod_put\u003c/span\u003e(dev, keypad\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003ekey[index].desc);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e dev_pm_ops meson_gpio_kp_pm_ops \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.suspend \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_suspend,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.resume \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_resume,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.freeze \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_freeze,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.thaw \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_resume,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.poweroff \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_suspend,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.restore \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_restore,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e platform_driver meson_gpio_kp_driver \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.probe \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_probe,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.remove \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e meson_gpio_kp_remove,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t.driver \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t.name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gpio-keypad\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t.pm \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003emeson_gpio_kp_pm_ops,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t.of_match_table \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e key_dt_match,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e __init \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_init\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eplatform_driver_register\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003emeson_gpio_kp_driver);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e __exit \u003cspan style=\"color:#a6e22e\"\u003emeson_gpio_kp_exit\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003eplatform_driver_unregister\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003emeson_gpio_kp_driver);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch1 id=\"逐函数解释\"\u003e逐函数解释\u003c/h1\u003e\n\u003cp\u003e以下按代码物理顺序，逐个函数解释它干了什么。\u003c/p\u003e","title":"Level 6 总结与扩展：逐函数拆解 GPIO 按键驱动"},{"content":"Level 5 扩展与总结：GPIO 子系统深度分析 本文是 level-5-GPIO.md 的延伸，记录从编写 fun_led.c 到发现 leds-gpio 现成方案过程中，对 GPIO 子系统的深度分析。\n1. 驱动分析：硬编码 GPIO 的问题 当前 fun_led.c 采用的是硬编码 GPIO 编号方式：\n#define LED_GPIO 28 /* 写死在代码里的 GPIO 编号 */ ... gpio_request(28, \u0026#34;fun_led\u0026#34;); gpio_set_value(28, val); 硬编码带来的三个风险 风险 1：内核升级，编号体系变化\nLED_GPIO 28 对应 S7D 的 GPIOD_4。这个 28 来自 meson-s7d-gpio.h 中 pins 数组的排列顺序。如果 Amlogic 在新内核 BSP 中调整了 pins 数组顺序（例如把 E 组排到 D 组之后），GPIOD_4 的编号就不再是 28 了。驱动里写死的 28 会指向错误的 pin。\n风险 2：换板子不可用\n同一份 fun_led.c 源码，想在另一个 S905 型号（比如 S6、S4D）上使用。S6 的 GPIOD_4 = 4，S4D 的 GPIOD_4 可能又是另一个编号。驱动里写着 28，换板子必炸。\n风险 3：驱动和硬件信息耦合\n驱动负责\u0026quot;控制 LED 亮灭\u0026quot;，但它不负责\u0026quot;知道 LED 接在哪个 pin 上\u0026quot;——这是硬件设计的决定，应该由硬件描述文件（DTS）来声明。当前代码把两件事揉在一起了。\n类比：餐厅点菜 硬编码方式： DTS 方式： 厨师口袋里揣着一张纸条 菜单写在墙上（DTS） \u0026#34;3 号桌客人要吃宫保鸡丁\u0026#34; 厨师看墙上菜单做菜 纸条上写着菜和桌号 换客人？改墙上的菜单即可 换客人？厨师换纸条 厨师不用换 当前 fun_led.c 就是那个\u0026quot;把桌号写死在口袋里\u0026quot;的厨师。\n2. DTS 驱动方案分析 目标：驱动不再硬编码 28，而是从 DTS 中读取\u0026quot;我控制的是哪个 GPIO\u0026quot;。\nDTS 侧：声明硬件连接 / { fun_led { compatible = \u0026#34;myvendor,fun-led\u0026#34;; led-gpios = \u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_HIGH\u0026gt;; }; }; 属性 含义 谁在用 compatible = \u0026quot;myvendor,fun-led\u0026quot; 设备型号标识 内核用它匹配对应的驱动 led-gpios 属性名，-gpios 后缀是内核约定 gpiod_get(dev, \u0026quot;led\u0026quot;, ...) 自动查找此属性 \u0026lt;\u0026amp;gpio ...\u0026gt; 引用 DTS 中的 gpio 控制器节点（bank@4000） of_parse_phandle 解析出 gpiochip GPIOD_4 pin 名称（不是数字 28！） 通过 #gpio-cells = \u0026lt;2\u0026gt; 解析，查 meson-s7d-gpio.h GPIO_ACTIVE_HIGH 高电平有效 gpiod 自动处理逻辑电平翻转 关键区别：硬编码版写的是 28（数字），DTS 版写的是 GPIOD_4（名称）。名称到数字的转换由内核在解析 DTS 时完成，驱动不感知。\n驱动侧：gpiod consumer API // ===== DTS 驱动版（应该写成这样）===== #include \u0026lt;linux/gpio/consumer.h\u0026gt; // 注意：consumer.h，不是 gpio.h #include \u0026lt;linux/of.h\u0026gt; // 读 DTS 属性 static int fun_led_probe(struct platform_device *pdev) { struct gpio_desc *led; // gpiod_get 自动查 DTS 中 \u0026#34;led-gpios\u0026#34; 属性 led = gpiod_get(\u0026amp;pdev-\u0026gt;dev, \u0026#34;led\u0026#34;, GPIOD_OUT_LOW); if (IS_ERR(led)) return PTR_ERR(led); // 此后只用 gpiod_set_value(led, 1/0)，完全不知道 pin 编号 ... } gpiod_get(\u0026amp;pdev-\u0026gt;dev, \u0026quot;led\u0026quot;, ...) 做的工作：\n1. 从 dev-\u0026gt;of_node 读 DTS 节点的 \u0026#34;led-gpios\u0026#34; 属性 ↓ 2. of_parse_phandle_with_args() 解析 \u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_HIGH\u0026gt; → args[0] = GPIOD_4 的编号（查 meson-s7d-gpio.h → 28） → args[1] = GPIO_ACTIVE_HIGH ↓ 3. of_get_named_gpiod_flags() 把解析结果转为 gpio_desc → gpio_to_desc(28) → 找到对应的 gpio_desc ↓ 4. gpiod_direction_output(desc, 0) → 配置为输出、初始低电平 ↓ 5. 返回 gpio_desc 指针给驱动 consumer.h vs gpio.h gpio.h 是 legacy 整数 API，操作的是\u0026quot;全局 GPIO 编号\u0026quot;：\ngpio_request(28, \u0026#34;fun_led\u0026#34;); // 28 是什么？查头文件才知道 gpio_direction_output(28, 0); gpio_set_value(28, 1); // 控制的是哪根 pin？看代码看不出来 gpio/consumer.h 是 gpiod API，操作的是\u0026quot;描述符\u0026quot;：\nstruct gpio_desc *led = gpiod_get(dev, \u0026#34;led\u0026#34;, GPIOD_OUT_LOW); // 描述符里有：pin 编号、所属 chip、方向、初始值…… gpiod_set_value(led, 1); // LED 亮——代码自解释 struct gpio_desc 里实际包含了：\nstruct gpio_desc { struct gpio_device *gdev; // → chip（指向 gpiochip0） unsigned long flags; // 状态标记（是否已申请、方向、开漏...） const char *label; // \u0026#34;fun_led\u0026#34;（gpio_request 时设置的标签） // ... }; // gdev→descs[hw_offset] = 指向自己的数组槽 // gdev→chip→set(chip, hw_offset, value) = 硬件写回调 驱动如何被 DTS 触发 probe？ // 匹配表：DTS 的 compatible 字符串 → 驱动 static const struct of_device_id fun_led_dt_ids[] = { { .compatible = \u0026#34;myvendor,fun-led\u0026#34; }, { } }; MODULE_DEVICE_TABLE(of, fun_led_dt_ids); // platform 驱动：不再用 module_init() 手动注册 static struct platform_driver fun_led_driver = { .probe = fun_led_probe, .remove = fun_led_remove, .driver = { .name = \u0026#34;fun_led\u0026#34;, .of_match_table = fun_led_dt_ids, }, }; module_platform_driver(fun_led_driver); 触发时机：\n内核启动 → of_platform_populate() 遍历 DTS 根节点下的子节点 → 找到 fun_led { compatible = \u0026#34;myvendor,fun-led\u0026#34; } → 遍历 platform_driver 注册表 → 匹配：fun_led_dt_ids 中有 \u0026#34;myvendor,fun-led\u0026#34; → 调用 fun_led_probe(\u0026amp;pdev) → gpiod_get(\u0026amp;pdev-\u0026gt;dev, \u0026#34;led\u0026#34;, ...) 拿到 GPIO → 注册字符设备 → /dev/fun_led 出现（自动创建，无需 insmod + mknod） 3. 实战：leds-gpio 完整流程追踪 发现过程 insmod fun_led.ko 报 -517（-EPROBE_DEFER），追查原因时发现：GPIOD_4 早已被内核的 leds-gpio 驱动通过 DTS 接管了。这意味着不必写任何新驱动，板子已经有一套完整的 DTS → 驱动 → sysfs → 硬件链路在运行。\n完整链路（6 步） /sys/class/leds/sys_led_red/brightness ← 用户写 brightness 控制 LED ↓ led-class.c:brightness_store() ← 接到用户写入的值 ↓ leds-gpio.c:gpio_led_set() ← 调用 gpiod_set_value() ↓ gpiolib.c:gpiod_set_value() ← 根据 GPIO_ACTIVE_LOW 翻转逻辑值 ↓ pinctrl-meson.c:meson_gpio_set() ← Amlogic 平台回调 ↓ regmap_update_bits(0xFE0040C4, BIT(4), v) ← 硬件寄存器写入 第 1 步：SoC 级 DTSI — 声明 GPIO 控制器硬件 文件：common/common_drivers/arch/arm64/boot/dts/amlogic/meson-s7d.dtsi:456-469\nperiphs_pinctrl: pinctrl@4000 { compatible = \u0026#34;amlogic,meson-s7d-periphs-pinctrl\u0026#34;; #address-cells = \u0026lt;2\u0026gt;; #size-cells = \u0026lt;2\u0026gt;; ranges; gpio: bank@4000 { reg = \u0026lt;0x0 0x4000 0x0 0x004c\u0026gt;, // mux 寄存器（pinmux 配置） \u0026lt;0x0 0x40c0 0x0 0x02cc\u0026gt;; // gpio 寄存器（输入/输出/中断） reg-names = \u0026#34;mux\u0026#34;, \u0026#34;gpio\u0026#34;; gpio-controller; // ← 标记 #gpio-cells = \u0026lt;2\u0026gt;; // ← 引用格式 \u0026lt;\u0026amp;gpio PIN FLAGS\u0026gt; gpio-ranges = \u0026lt;\u0026amp;periphs_pinctrl 0 0 84\u0026gt;; }; }; gpio-controller — 其他设备可以通过 gpios 属性引用此节点 #gpio-cells = \u0026lt;2\u0026gt; — 引用需 2 个 cell：pin 编号 + flags gpio-ranges — GPIO pin ↔ pinctrl pin 映射表 标签 gpio: 是别名，DTS 中写 \u0026amp;gpio 即引用此节点 第 2 步：板级 DTS — 声明 LED 设备 文件：common/common_drivers/arch/arm64/boot/dts/amlogic/s7d_s905x5m_bm201.dts:216-229\ngpio_leds { compatible = \u0026#34;gpio-leds\u0026#34;; status = \u0026#34;okay\u0026#34;; sys_led_red { label = \u0026#34;sys_led_red\u0026#34;; gpios = \u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;on\u0026#34;; }; }; 属性 DTS 中写的 内核解析结果 compatible \u0026quot;gpio-leds\u0026quot; 匹配 of_gpio_leds_match[] gpios \u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_LOW\u0026gt; pin 28, FLAG_ACTIVE_LOW default-state \u0026quot;on\u0026quot; 驱动 init 时点亮 label \u0026quot;sys_led_red\u0026quot; sysfs 目录名 内核如何读取这些 DTS 属性？—— OF API 概览 DTS 文件编译成 DTB（Device Tree Blob）后由 bootloader 传给内核。内核提供了一套 OF (Open Firmware) API，驱动通过这些函数读取 DTS 中声明的属性。每个属性类型有对应的 API：\nDTS 属性 类型 内核读取 API 谁调用 compatible 字符串 of_match_device() → 遍历 of_match_table platform bus（自动） gpios phandle 引用 of_parse_phandle_with_args_map() gpiolib-of.c default-state 字符串 fwnode_property_read_string(fwnode, \u0026quot;default-state\u0026quot;, ...) leds-gpio.c → led_init_default_state_get() label 字符串 fwnode_property_read_string()（LED core 内部） led-class.c → led_compose_name() status 字符串 of_device_is_available() platform bus（自动） 下面逐一追踪每种属性的读取过程。\ngpios 属性解析：\u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_LOW\u0026gt; → gpio_desc* 这是最复杂的一个。leds-gpio.c 调用的入口是：\nled.gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child, GPIOD_ASIS, NULL); 完整调用链（每个函数标注文件路径）：\n[1] leds-gpio.c:154 devm_fwnode_get_gpiod_from_child(dev, NULL, child, GPIOD_ASIS, NULL) │ ▼ [2] gpiolib.c:4013 fwnode_get_named_gpiod(fwnode, NULL, index, dflags, label) → is_of_node(fwnode)? YES → 走 OF 路径 │ ▼ [3] gpiolib-of.c:358 gpiod_get_from_of_node(to_of_node(fwnode), propname, index, dflags, label) → propname 为 NULL，自动推导属性名为 \u0026#34;gpios\u0026#34;（-gpios 后缀是内核约定） │ ▼ [4] gpiolib-of.c:291 of_get_named_gpiod_flags(node, \u0026#34;gpios\u0026#34;, index, \u0026amp;flags) │ ├─ [4a] drivers/of/base.c │ of_parse_phandle_with_args_map(np, \u0026#34;gpios\u0026#34;, \u0026#34;gpio\u0026#34;, 0, \u0026amp;gpiospec) │ 解析 DTS 属性 \u0026#34;gpios = \u0026lt;\u0026amp;gpio 28 1\u0026gt;\u0026#34;: │ gpiospec.np → gpio 控制器的 device_node (bank@4000) │ gpiospec.args[0] → 28 (GPIOD_4 宏展开后的值) │ gpiospec.args[1] → 1 (GPIO_ACTIVE_LOW) │ gpiospec.args_count → 2 (由 #gpio-cells = \u0026lt;2\u0026gt; 决定) │ ├─ [4b] gpiolib-of.c:93 of_find_gpiochip_by_xlate(\u0026amp;gpiospec) │ → gpiochip_find(gpiospec, of_gpiochip_match_node_and_xlate) │ 遍历所有已注册的 gpio_chip，找到 of_node == gpiospec.np 的那个 │ → 返回 gpiochip0（由 bank@4000 注册的 chip） │ └─ [4c] gpiolib-of.c:99 of_xlate_and_get_gpiod_flags(chip, \u0026amp;gpiospec, \u0026amp;flags) → chip-\u0026gt;of_gpio_n_cells != args_count? 2 == 2 ✓ → chip-\u0026gt;of_xlate(chip, \u0026amp;gpiospec, \u0026amp;flags) → of_gpio_simple_xlate() [gpiolib-of.c:851] if (gpiospec-\u0026gt;args[0] \u0026gt;= gc-\u0026gt;ngpio) → 28 \u0026lt; 83 ✓ *flags = gpiospec-\u0026gt;args[1] → flags = 1 (GPIO_ACTIVE_LOW) return gpiospec-\u0026gt;args[0] → 返回 28 → gpiochip_get_desc(chip, 28) → 返回 \u0026amp;chip-\u0026gt;gpiodev-\u0026gt;descs[28] ← 这就是 gpio_desc*！ 关键点总结：\nof_parse_phandle_with_args_map() — 把 DTS 的属性值 \u0026lt;\u0026amp;gpio 28 1\u0026gt; 解析成结构化数据（of_phandle_args），包括引用的节点指针（np）和参数数组（args[]） of_find_gpiochip_by_xlate() — 根据 phandle 找到对应的 gpio_chip（哪个芯片的 GPIO 控制器） of_xlate() — chip 的\u0026quot;翻译\u0026quot;回调，把 args[] 翻译成 chip 内部的 offset（这里 28 → 28，直通） gpiochip_get_desc() — 用 offset 从 chip 的描述符数组中取出 gpio_desc* of_parse_phandle_with_args_map() 的关键参数：\n\u0026quot;gpios\u0026quot; — DTS 中的属性名 \u0026quot;gpio\u0026quot; — 属性值中 phandle cell 的名字（用于查找 #gpio-cells） 它会找到 \u0026amp;gpio 引用的节点（bank@4000），读该节点的 #gpio-cells = \u0026lt;2\u0026gt;，知道\u0026quot;后面 2 个 cell 是参数\u0026quot;，然后把 \u0026lt;28 1\u0026gt; 填入 args[0] 和 args[1]。\ndefault-state 属性解析：\u0026quot;on\u0026quot; → LEDS_DEFSTATE_ON 文件：common/drivers/leds/led-core.c:481-493\nenum led_default_state led_init_default_state_get(struct fwnode_handle *fwnode) { const char *state = NULL; // ★ 从 fwnode 读 \u0026#34;default-state\u0026#34; 字符串属性 if (!fwnode_property_read_string(fwnode, \u0026#34;default-state\u0026#34;, \u0026amp;state)) { if (!strcmp(state, \u0026#34;keep\u0026#34;)) return LEDS_DEFSTATE_KEEP; if (!strcmp(state, \u0026#34;on\u0026#34;)) return LEDS_DEFSTATE_ON; // ← \u0026#34;on\u0026#34; → 返回此值 } return LEDS_DEFSTATE_OFF; // 缺省值：off } fwnode_property_read_string()（drivers/base/property.c:392）是固件无关的属性读取 API，对于 OF 设备，内部调用 of_property_read_string() → 直接从 DTB 中的字符串表读取。\nlabel 属性解析：\u0026quot;sys_led_red\u0026quot; → LED 设备名 label 不由 leds-gpio.c 直接读取，而是通过 LED core 的 led_compose_name() 处理：\n文件：common/drivers/leds/led-core.c\n// 简化逻辑 led_compose_name(parent, init_data, fwnode) → fwnode_property_read_string(fwnode, \u0026#34;label\u0026#34;, \u0026amp;name) // 读 \u0026#34;label\u0026#34; 属性 → 若不存在 label，则用 function + color 或 node 名拼接 → 返回 name → 用于创建 /sys/class/leds/\u0026lt;name\u0026gt;/ compatible 属性匹配：\u0026quot;gpio-leds\u0026quot; → 驱动 probe 这不由驱动代码主动调用，而是由 platform bus 框架自动完成：\nplatform 总线枚举 DTS 节点 → of_match_device(drv-\u0026gt;of_match_table, dev-\u0026gt;of_node) → __of_match_node(matches, node) 遍历 matches[]，对每个 entry: → of_compat_cmp(node, match-\u0026gt;compatible) 比较 DTS compatible 字符串与 match-\u0026gt;compatible 匹配成功 → 返回匹配的 of_device_id* → 调用驱动的 .probe() 回调 文件：drivers/of/device.c\n总结：DTS → 驱动 数据通路全景 DTS 文件 (源码) 内核 API 驱动变量 ──────────────────────────────────────────────────────────────────────── compatible = \u0026#34;gpio-leds\u0026#34; → of_match_device() → 触发 probe label = \u0026#34;sys_led_red\u0026#34; → fwnode_property_read_string() → cdev.name gpios = \u0026lt;\u0026amp;gpio → of_parse_phandle_with_args_map() → gpiospec GPIOD_4 → → args[0]=28 GPIO_ACTIVE_LOW\u0026gt; → → args[1]=1 → of_find_gpiochip_by_xlate() → gpiochip0 → chip-\u0026gt;of_xlate() → offset=28 → gpiochip_get_desc() → gpio_desc* default-state = \u0026#34;on\u0026#34; → fwnode_property_read_string() → LEDS_DEFSTATE_ON retain-state-suspended → fwnode_property_present() → bool true 所有 API 的入口都是 fwnode_*（固件节点），对 OF 设备自动转发到 of_* 实现。这样驱动代码不必关心底层是 OF 还是 ACPI——同一套 API 通吃。\n第 3 步：驱动匹配 文件：common/drivers/leds/leds-gpio.c:187-308\nstatic const struct of_device_id of_gpio_leds_match[] = { { .compatible = \u0026#34;gpio-leds\u0026#34; }, // ← 匹配 DTS {}, }; static struct platform_driver gpio_led_driver = { .probe = gpio_led_probe, .driver = { .name = \u0026#34;leds-gpio\u0026#34;, .of_match_table = of_gpio_leds_match, }, }; module_platform_driver(gpio_led_driver); 触发：内核启动 → of_platform_populate() → 找到 compatible = \u0026quot;gpio-leds\u0026quot; → gpio_led_probe()。\n第 4 步：GPIO 获取 文件：common/drivers/leds/leds-gpio.c:130-185\nstatic struct gpio_leds_priv *gpio_leds_create(struct platform_device *pdev) { device_for_each_child_node(dev, child) { // ★ 从 DTS 子节点获取 gpio_desc led.gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child, GPIOD_ASIS, NULL); // 内部调用链： // → gpiod_get_from_of_node() // → of_get_named_gpiod_flags() 解析 \u0026#34;gpios\u0026#34; 属性 // → of_parse_phandle_with_args() \u0026lt;\u0026amp;gpio 28 GPIO_ACTIVE_LOW\u0026gt; // → gpio_to_desc(28) → gpio_desc* led.default_state = led_init_default_state_get(child); create_gpio_led(\u0026amp;led, led_dat, dev, child, NULL); } } 第 5 步：LED 注册 → sysfs 文件：common/drivers/leds/leds-gpio.c:75-123\nstatic int create_gpio_led(...) { led_dat-\u0026gt;cdev.brightness_set = gpio_led_set; // 回调函数 led_dat-\u0026gt;cdev.max_brightness = 1; // GPIO LED 只有 0/1 state = (template-\u0026gt;default_state == LEDS_GPIO_DEFSTATE_ON); // \u0026#34;on\u0026#34; → 1 gpiod_direction_output(led_dat-\u0026gt;gpiod, state); // 配置 GPIO 输出 devm_led_classdev_register_ext(parent, \u0026amp;led_dat-\u0026gt;cdev, \u0026amp;init_data); // → 创建 /sys/class/leds/sys_led_red/ } 第 6 步：用户态 → 硬件寄存器（完整代码调用链） 用户态: echo 1 \u0026gt; /sys/class/leds/sys_led_red/brightness │ ▼ [1] common/drivers/leds/led-class.c:38-65 brightness_store() kstrtoul(\u0026#34;1\u0026#34;) → state = 1 led_set_brightness(led_cdev, 1) │ ▼ [2] common/drivers/leds/leds-gpio.c:35-56 gpio_led_set(cdev, LED_ON) level = (value == LED_OFF) ? 0 : 1 → level = 1 gpiod_set_value(led_dat-\u0026gt;gpiod, 1) │ ▼ [3] common/drivers/gpio/gpiolib.c gpiod_set_value_nocheck(desc, 1) → 检测 FLAG_ACTIVE_LOW: value = !value → 物理值 = 0（翻转！） → gpiod_set_raw_value_commit(desc, 0) │ ▼ [4] common/drivers/gpio/gpiolib.c:2854 gc = desc-\u0026gt;gdev-\u0026gt;chip gc-\u0026gt;set(gc, gpio_chip_hwgpio(desc), 0) │ ▼ [5] common/common_drivers/drivers/gpio/ pinctrl/pinctrl-meson.c:582 meson_gpio_set(chip, 28, 0) → meson_get_bank(pc, 28, \u0026amp;bank) pin 28 ∈ [24,28] → Bank D → meson_calc_reg_and_bit(...) reg = 0x0C4, bit = 4 → regmap_update_bits(reg_gpio, BIT(4) = 0 → 引脚拉低 0x0C4, BIT(4), 0) │ ▼ [6] 硬件 GPIO 输出寄存器 @ 0xFE0040C4 bit4 写 0 → GPIOD_4 物理电平 LOW 因为 active-low 接线： LOW on 阴极 → LED 亮 ✓ GPIO_ACTIVE_LOW 的含义：gpios = \u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_LOW\u0026gt; 告诉内核\u0026quot;低电平有效\u0026quot;。内核在 gpiod_set_value_nocheck() 中自动翻转：用户写 brightness=1 → gpiod_set_value(1) → 检测 FLAG_ACTIVE_LOW → value = 0 → 物理写 LOW → LED 亮。调用者无需关心 active-high/low。\nfun_led.ko 为何失败（-EPROBE_DEFER 根因） 层面 1：资源冲突（根本原因）\nGPIOD_4 已在 DTS 中声明为 gpio-leds 的子节点 sys_led_red。内核启动时 leds-gpio 驱动通过 gpiod_get() 申请了该 pin。当 fun_led.ko 再尝试 gpio_request(28) 时，gpiolib 检测到 FLAG_REQUESTED 已置位 → 应返回 -EBUSY。\n层面 2：为什么报 -517 而非 -EBUSY\ngpiod_request() 返回值初始值就是 -EPROBE_DEFER（gpiolib.c:1988）：\nint gpiod_request(struct gpio_desc *desc, const char *label) { int ret = -EPROBE_DEFER; // ← 初始值 = -517 if (try_module_get(gdev-\u0026gt;owner)) { // ← GPIO chip owner 模块可用？ ret = gpiod_request_commit(desc, label); // 进入实际申请 } return ret; } 在 GKI 架构下，gpio_chip 的 owner 指向 vendor 模块。try_module_get() 失败时维持 -517，不会进入 gpiod_request_commit() → 不会触发\u0026quot;已占用\u0026quot;的 -EBUSY 检查。\nfun_led.ko 被两层拒绝：第一层模块依赖检查（-517），第二层资源冲突（-EBUSY）。最终返回第一层的 -517。\n两种方案对比 硬编码版（fun_led.c） DTS + gpio-leds（现成方案） ──────────────────── ────────────────────────── GPIO 信息位置 驱动代码 #define LED_GPIO DTS gpios = \u0026lt;\u0026amp;gpio GPIOD_4 ...\u0026gt; GPIO 标识方式 数字 28 名称 GPIOD_4（跨 SoC 不变） 驱动代码 需要自己写 内核已有，零代码 用户接口 /dev/fun_led /sys/class/leds/sys_led_red/ echo 1 \u0026gt; /dev/fun_led echo 1 \u0026gt; brightness 接口能力 只有 0/1 支持 trigger（timer/heartbeat/...） 支持 default-state 申请 GPIO 方式 gpio_request(28, ...) gpiod_get(dev, \u0026#34;led\u0026#34;, ...) legacy 整数 API gpiod consumer API 驱动入口 module_init() module_platform_driver() 手动 insmod compatible 自动触发 probe 可移植性 差 好 换 SoC 改 C 代码 换板子只改 DTS 4. 深入理解：gpio_set_value(28, 0) 如何定位到硬件寄存器 从 全局 GPIO 编号 28 到 硅片上的寄存器 bit，经过 4 层查表。\n形象类比：写字楼找工位 你 gpio_set_value(28, 0) │ ▼ 前台 gpio_to_desc(28) │ \u0026#34;28 号员工？我帮你查...\u0026#34; │ 遍历 gpio_devices 链表 │ 找到负责 #28 的 gpiochip → 返回工牌（gpio_desc） │ ▼ 工牌 gpio_desc { │ .gdev→chip = 指向管理这个 pin 的 gpio_chip │ 硬件编号 = 28（chip 内的 offset） │ } │ ▼ 部门经理 gc-\u0026gt;set(chip, 28, 0) │ \u0026#34;我是 GPIO 控制器，我来处理 #28\u0026#34; │ 调用 Amlogic pinctrl 驱动的 meson_gpio_set() │ ▼ 楼层表 meson_get_bank(pin=28) │ 查 bank 表 ←── 这就是你要找的「偏移表」！ │ ┌─────────────────────────────────────────┐ │ │ Bank │ first │ last │ out寄存器 │ bit │ │ │ E │ 0 │ 1 │ 0x041 │ 0 │ │ │ B │ 2 │ 15 │ 0x061 │ 0 │ │ │ C │ 16 │ 23 │ 0x051 │ 0 │ │ │ D │ 24 │ 28 │ 0x031 │ 0 │ ← pin28 命中! │ │ DV │ 29 │ 35 │ 0x071 │ 0 │ │ │ ... │ ... │ ... │ ... │ ... │ │ └─────────────────────────────────────────┘ │ ▼ 计算器 meson_calc_reg_and_bit() │ pin = 28, bank-\u0026gt;first = 24 │ bit = (0 + 28 - 24) × 1 = 4 │ reg = (0x031 + 4/32) × 4 = 0x0C4 │ ▼ 门禁刷卡 regmap_update_bits(reg_gpio, 0x0C4, BIT(4), 0) │ 绝对地址 = 0xfe0040c0 + 0x0C4 = 0xfe004184 │ 把 bit4 写 0 → 引脚拉低 代码调用链（与上面一一对应） gpio_set_value(28, 0) ← gpio.h:69，代码入口 gpiod_set_raw_value(gpio_to_desc(28), 0) ← 前台：查号 → 拿工牌 gpio_to_desc(28) ← gpiolib.c:108，遍历 gpio_devices → 找到 gdev，返回 \u0026amp;gdev-\u0026gt;descs[28 - gdev-\u0026gt;base] gpiod_set_raw_value_commit(desc, 0) ← gpiolib.c:2854 gc = desc-\u0026gt;gdev-\u0026gt;chip ← 取 gpio_chip gc-\u0026gt;set(gc, gpio_chip_hwgpio(desc), 0) ← 调部门经理 meson_gpio_set(chip, 28, 0) ← pinctrl-meson.c:582 meson_pinconf_set_drive(pc, 28, 0) meson_pinconf_set_gpio_bit(pc, 28, REG_OUT, 0) meson_get_bank(pc, 28, \u0026amp;bank) ← 查楼层表：pin28→Bank D meson_calc_reg_and_bit() ← 算出 reg=0x0C4, bit=4 regmap_update_bits(..., 0x0C4, BIT(4), 0) ← 写硬件! bank 表在代码中的具体位置 S7D 表定义 — common/common_drivers/drivers/gpio/pinctrl/pinctrl-meson-s7d.c:1323-1343：\nstatic struct meson_bank meson_s7d_periphs_banks[] = { // name first last ... dir out in BANK_DS(\u0026#34;E\u0026#34;, GPIOE_0, GPIOE_1, ... 0x042,0, 0x041,0, 0x040,0), BANK_DS(\u0026#34;B\u0026#34;, GPIOB_0, GPIOB_13, ... 0x062,0, 0x061,0, 0x060,0), BANK_DS(\u0026#34;C\u0026#34;, GPIOC_0, GPIOC_7, ... 0x052,0, 0x051,0, 0x050,0), BANK_DS(\u0026#34;D\u0026#34;, GPIOD_0, GPIOD_4, ... 0x032,0, 0x031,0, 0x030,0), ← pin 28 BANK_DS(\u0026#34;DV\u0026#34;, GPIODV_0, GPIODV_6, ... 0x072,0, 0x071,0, 0x070,0), BANK_DS(\u0026#34;H\u0026#34;, GPIOH_0, GPIOH_11, ... 0x022,0, 0x021,0, 0x020,0), BANK_DS(\u0026#34;X\u0026#34;, GPIOX_0, GPIOX_19, ... 0x012,0, 0x011,0, 0x010,0), BANK_DS(\u0026#34;Z\u0026#34;, GPIOZ_0, GPIOZ_12, ... 0x002,0, 0x001,0, 0x000,0), }; 查表函数 — common/drivers/pinctrl/meson/pinctrl-meson.c:72-106：\nstatic int meson_get_bank(struct meson_pinctrl *pc, unsigned int pin, struct meson_bank **bank) { for (i = 0; i \u0026lt; pc-\u0026gt;data-\u0026gt;num_banks; i++) if (pin \u0026gt;= pc-\u0026gt;data-\u0026gt;banks[i].first \u0026amp;\u0026amp; pin \u0026lt;= pc-\u0026gt;data-\u0026gt;banks[i].last) { *bank = \u0026amp;pc-\u0026gt;data-\u0026gt;banks[i]; return 0; } } static void meson_calc_reg_and_bit(struct meson_bank *bank, unsigned int pin, enum meson_reg_type reg_type, unsigned int *reg, unsigned int *bit) { struct meson_reg_desc *desc = \u0026amp;bank-\u0026gt;regs[reg_type]; *bit = (desc-\u0026gt;bit + pin - bank-\u0026gt;first) * meson_bit_strides[reg_type]; *reg = (desc-\u0026gt;reg + (*bit / 32)) * 4; *bit \u0026amp;= 0x1f; } 为什么 GPIOD_4(28) 不会和 GPIOB_4(6) 或 GPIOC_4(20) 冲突？ bank 表用 first/last 划定了唯一区间：\nGPIOD_4 = 28 → 28 ∈ [24, 28] → Bank D GPIOB_4 = 6 → 6 ∈ [ 2, 15] → Bank B GPIOC_4 = 20 → 20 ∈ [16, 23] → Bank C 不同 bank → 不同寄存器偏移 → 不同 bit。全局编号 28 只属于 Bank D。\n5. /sys/class/leds/sys_led_red/ 下的文件是怎么来的？ /sys/class/leds/sys_led_red/ ├── brightness ← LED class 属性组 ├── max_brightness ← LED class 属性组 ├── trigger ← LED trigger 属性组 ├── device ← 设备模型 symlink（内核通用） ├── subsystem ← 设备模型 symlink（内核通用） ├── uevent ← 设备模型标准属性（内核通用） └── power/ ← 电源管理 sysfs（内核通用） 三类来源，由 device_add() 一站式组装。\n① LED class 属性（brightness、max_brightness、trigger） 文件：common/drivers/leds/led-class.c:77-104\n// 普通属性组 static struct attribute *led_class_attrs[] = { \u0026amp;dev_attr_brightness.attr, // DEVICE_ATTR_RW(brightness) \u0026amp;dev_attr_max_brightness.attr, // DEVICE_ATTR_RO(max_brightness) NULL, }; static const struct attribute_group led_group = { .attrs = led_class_attrs, }; // trigger 是二进制属性（内容可能很长），单独成组 static BIN_ATTR(trigger, 0644, led_trigger_read, led_trigger_write, 0); static const struct attribute_group led_trigger_group = { .bin_attrs = led_trigger_bin_attrs, }; // 打包注册到 class static const struct attribute_group *led_groups[] = { \u0026amp;led_group, // → brightness, max_brightness \u0026amp;led_trigger_group, // → trigger NULL, }; 绑定发生在 led-class.c:539：leds_class-\u0026gt;dev_groups = led_groups;\n设备创建时，device_add() 自动应用 class 的 dev_groups。\n② 设备模型标准文件（uevent、device、subsystem） 文件：common/drivers/base/core.c:3361-3368\nint device_add(struct device *dev) { error = device_create_file(dev, \u0026amp;dev_attr_uevent); // → uevent error = device_add_class_symlinks(dev); // → device, subsystem error = device_add_attrs(dev); // → 应用 ① 的 led_groups error = dpm_sysfs_add(dev); // → power/ } device_add_class_symlinks()（core.c:3122-3150）：\nsysfs_create_link(\u0026amp;dev-\u0026gt;kobj, \u0026amp;dev-\u0026gt;class-\u0026gt;p-\u0026gt;subsys.kobj, \u0026#34;subsystem\u0026#34;); sysfs_create_link(\u0026amp;dev-\u0026gt;kobj, \u0026amp;dev-\u0026gt;parent-\u0026gt;kobj, \u0026#34;device\u0026#34;); ③ power/ 目录 文件：common/drivers/base/power/sysfs.c\ndpm_sysfs_add() 创建 power/ 子目录，内含 control、autosuspend_delay_ms、wakeup 等文件。\n整体注册链 devm_led_classdev_register_ext() ← leds-gpio.c 调用 → device_create_with_groups(leds_class, ...) ← 传入 led_groups → device_register() → device_add() ← ★ 核心函数 ├─ device_create_file(uevent) → uevent ├─ device_add_class_symlinks() → device, subsystem ├─ device_add_attrs() → 遍历 class-\u0026gt;dev_groups │ ├─ led_group → brightness, max_brightness │ └─ led_trigger_group → trigger ├─ dpm_sysfs_add() → power/ └─ kobject_uevent(KOBJ_ADD) → 通知 udev 设计要点：驱动只声明\u0026quot;这类设备有什么特有属性\u0026quot;（dev_groups），通用文件由内核设备模型统一提供。一套机制，所有 class 共用。\n6. 如何查询 GPIO 名称的全局编号？ 问题：原理图标 GPIOD_4，但 API 要整数。怎么知道 GPIOD_4 = 28？\n答案在 dt-bindings 头文件 — common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h：\n/* GPIOE */ ← S7D 以 E 组起始 #define GPIOE_0 0 ... /* GPIOB */ #define GPIOB_0 2 ... /* GPIOC */ #define GPIOC_0 16 ... /* GPIOD */ #define GPIOD_0 24 #define GPIOD_4 28 ← S7D 上 GPIOD_4 = 28 /* GPIOX */ #define GPIOX_0 48 ... 编号规则：全局编号 = 该 pin 在 meson_s7d_periphs_pins[] 数组中的索引位置。S7D 顺序为 E→B→C→D→DV→H→X→Z→TEST_N→CC（83 pins）。\n查找方法：\ngrep GPIOD_4 common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h grep \u0026#34;MESON_PIN\u0026#34; common/common_drivers/drivers/gpio/pinctrl/pinctrl-meson-s7d.c | head -35 不同 SoC 编号完全不同：\n芯片 GPIOD_4 编号 GPIOA_4 编号 总 pins S6 4 86 101 S7D 28 不存在 83 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-5-%E6%80%BB%E7%BB%93%E4%B8%8E%E6%89%A9%E5%B1%95/","summary":"\u003ch1 id=\"level-5-扩展与总结gpio-子系统深度分析\"\u003eLevel 5 扩展与总结：GPIO 子系统深度分析\u003c/h1\u003e\n\u003cp\u003e本文是 \u003ca href=\"level-5-GPIO.md\"\u003elevel-5-GPIO.md\u003c/a\u003e 的延伸，记录从编写 \u003ccode\u003efun_led.c\u003c/code\u003e 到发现 \u003ccode\u003eleds-gpio\u003c/code\u003e 现成方案过程中，对 GPIO 子系统的深度分析。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-驱动分析硬编码-gpio-的问题\"\u003e1. 驱动分析：硬编码 GPIO 的问题\u003c/h2\u003e\n\u003cp\u003e当前 \u003ccode\u003efun_led.c\u003c/code\u003e 采用的是\u003cstrong\u003e硬编码 GPIO 编号\u003c/strong\u003e方式：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#define LED_GPIO  28   \u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e/* 写死在代码里的 GPIO 编号 */\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003egpio_request\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e28\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;fun_led\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003egpio_set_value\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e28\u003c/span\u003e, val);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"硬编码带来的三个风险\"\u003e硬编码带来的三个风险\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e风险 1：内核升级，编号体系变化\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eLED_GPIO 28\u003c/code\u003e 对应 S7D 的 GPIOD_4。这个 28 来自 \u003ccode\u003emeson-s7d-gpio.h\u003c/code\u003e 中 pins 数组的排列顺序。如果 Amlogic 在新内核 BSP 中调整了 pins 数组顺序（例如把 E 组排到 D 组之后），GPIOD_4 的编号就不再是 28 了。驱动里写死的 28 会指向错误的 pin。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e风险 2：换板子不可用\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e同一份 \u003ccode\u003efun_led.c\u003c/code\u003e 源码，想在另一个 S905 型号（比如 S6、S4D）上使用。S6 的 GPIOD_4 = 4，S4D 的 GPIOD_4 可能又是另一个编号。驱动里写着 28，换板子必炸。\u003c/p\u003e","title":"Level 5 扩展与总结：GPIO 子系统深度分析"},{"content":"Level 5：操作 GPIO 我想做 控制板子上的一个物理引脚，让它输出高/低电平，用万用表或 LED 确认电平变化。\n先回答三个问题 GPIO 在系统里是以什么方式呈现的？\n物理上：芯片的一个管脚，可以设置输出高/低电平或读取输入电平 代码上：GPIO controller 是一个硬件模块，通过寄存器操作 给用户的接口：sysfs 或 libgpiod，或者自己写驱动操作寄存器 设备树（DTS）和驱动是什么关系？\nDTS 描述硬件有什么、接在哪个总线、地址/中断号是什么 驱动根据 DTS 的信息来初始化硬件 同一个驱动可以支持多个 DTS 描述的实例 你的板子的 GPIO 信息从哪儿来？\n原理图（PDF）——告诉你在板子上哪个排针/接口引出了哪个 GPIO DTS——告诉内核哪个 GPIO controller 对应哪个基地址 datasheet——详细到每个寄存器的位定义 需要懂的知识 GPIO 子系统\n内核用 gpiolib 统一管理 GPIO。用户态访问 GPIO 有两种方式：\n旧方式（sysfs，kernel \u0026lt; 5.10）：\necho 100 \u0026gt; /sys/class/gpio/export echo out \u0026gt; /sys/class/gpio/gpio100/direction echo 1 \u0026gt; /sys/class/gpio/gpio100/value 新方式（libgpiod，kernel \u0026gt;= 5.10）：\ngpioinfo gpioset gpiochip0 100=1 你的内核是 5.15，优先用 libgpiod。\nioremap 的作用\n驱动不能直接访问物理地址（因为 MMU 开启了虚拟地址映射）。ioremap 将物理地址映射到内核虚拟地址空间：\nvoid __iomem *base = ioremap(0xff800000, 0x1000); writel(1, base + 0x14); readl(base + 0x14); 没有 ioremap 会怎样？直接访问物理地址会触发 kernel panic。\nDTS 中的 GPIO 描述\ngpio@ff800000 { compatible = \u0026#34;amlogic,meson-g12a-gpio\u0026#34;; reg = \u0026lt;0x0 0xff800000 0x0 0x1000\u0026gt;; gpio-controller; #gpio-cells = \u0026lt;2\u0026gt;; }; reg —— 寄存器基地址和范围 gpio-controller —— 标记这是一个 GPIO 控制器 #gpio-cells —— 每个 GPIO 描述需要几个 cell（通常 2：引脚号 + 标志位） 如何查询某个 GPIO 名称的全局编号？\n问题：原理图上标的是 GPIOD_4，但 gpio_set_value() 需要的是一个整数。我怎么知道 GPIOD_4 = 28？\n答案在 dt-bindings 头文件。每个 SoC 有一个 xxx-gpio.h 文件，为所有 GPIO pin 分配全局唯一编号：\n文件：common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h\n/* GPIOE */ ← S7D 以 E 组起始 #define GPIOE_0 0 ... /* GPIOB */ #define GPIOB_0 2 ... /* GPIOC */ #define GPIOC_0 16 ... /* GPIOD */ #define GPIOD_0 24 #define GPIOD_4 28 ← 这就是你要找的！S7D 上 GPIOD_4 = 28 /* GPIOX */ #define GPIOX_0 48 ... 编号规则：全局编号 = 该 pin 在 meson_s7d_periphs_pins[] 数组中的索引位置。S7D 顺序为 E→B→C→D→DV→H→X→Z→TEST_N→CC（共 83 pins），与 S6 不同（S6 是 D→F→E→B→C→X→H→Z→A→TEST_N→CC，101 pins）。\n查找方法：\n# 方法 1：直接看头文件 grep GPIOD_4 common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h # 方法 2：在 pinctrl 驱动中看 pins 数组的排列顺序（索引从 0 起） grep \u0026#34;MESON_PIN\u0026#34; common/common_drivers/drivers/gpio/pinctrl/pinctrl-meson-s7d.c | head -35 不同 SoC 的编号完全不同，必须查对应当前芯片的头文件！\n芯片 GPIOD_4 编号 GPIOA_4 编号 总 pins S6 4 86 101 S7D 28 不存在 83 动手方案 前提 板子：S905X5M (S7D)，内核 5.15 目标 GPIO：GPIOD_4（pin 28），原理图标号 FUN_LED GPIO 控制器：/dev/gpiochip0（bank@4000），83 pins，基址 0xfe004000 重要发现：GPIOD_4 已被 leds-gpio 驱动接管（DTS 中声明为 sys_led_red），可直接通过 sysfs 控制，无需写驱动 方式 A（推荐）：通过现有 sysfs LED 接口控制（零代码） GPIOD_4 在板级 DTS s7d_s905x5m_bm201.dts:224-228 中已声明为 gpio-leds 设备的子节点 sys_led_red。内核 leds-gpio 驱动已接管该 pin，提供标准 sysfs 接口：\n# 直接控制 LED 亮灭（无需 insmod / mknod） adb shell \u0026#34;echo 1 \u0026gt; /sys/class/leds/sys_led_red/brightness\u0026#34; # LED 亮 adb shell \u0026#34;echo 0 \u0026gt; /sys/class/leds/sys_led_red/brightness\u0026#34; # LED 灭 # 查看当前状态 adb shell \u0026#34;cat /sys/class/leds/sys_led_red/brightness\u0026#34; # 设置心跳闪烁 adb shell \u0026#34;echo heartbeat \u0026gt; /sys/class/leds/sys_led_red/trigger\u0026#34; # 恢复手动控制 adb shell \u0026#34;echo none \u0026gt; /sys/class/leds/sys_led_red/trigger\u0026#34; # 验证底层 GPIO 确实被占用 adb shell \u0026#34;cat /sys/kernel/debug/gpio\u0026#34; # 会看到 gpio-28 标为 \u0026#34;sys_led_red\u0026#34; 完整调用链路（详细分析见下一节）：\necho 1 \u0026gt; /sys/class/leds/sys_led_red/brightness → led-class.c:brightness_store() → leds-gpio.c:gpio_led_set() → gpiolib.c:gpiod_set_value() // FLAG_ACTIVE_LOW 自动翻转 → pinctrl-meson.c:meson_gpio_set() → regmap_update_bits() // 写 GPIO 输出寄存器 方式 B：编写 fun_led.c 字符设备驱动（学习用，需先解除 GPIO 占用） 注意：此方式与现有 leds-gpio 冲突。若强行 insmod 会报 -517（-EPROBE_DEFER），详见 debug.md [L5-02]。如需测试此方式，须先在 DTS 中 disable sys_led_red 节点后重新编译烧录。\n以下是原方案（仅供理解构建流程）：\n第 1 步：确认 GPIO 子系统正常 adb shell ls /dev/gpiochip0 # 确认 gpiochip 存在 adb shell cat /sys/bus/gpio/devices/gpiochip0/uevent # → OF_NAME=bank # → OF_FULLNAME=/soc/apb4@fe000000/pinctrl@4000/bank@4000 第 2 步：从原理图/DTS 确认 GPIO 信息 原理图截图中 GPIOD_4 标为 FUN_LED。交叉验证 DTS 中该 pin 未被占用：\n# 在对应的 S7D DTS 中确认 GPIOD_4 未被占用 grep \u0026#34;GPIOD_4\u0026#34; common/common_drivers/arch/arm64/boot/dts/amlogic/你板子对应的.dts 第 3 步：编写驱动 fun_led.c 基于 Level 4 的字符设备框架，加入 GPIO 控制。驱动代码：\n文件：common/drivers/misc/fun_led.c 用 \u0026lt;linux/gpio.h\u0026gt; 的 legacy API：gpio_request(28, \u0026quot;fun_led\u0026quot;) → gpio_direction_output(28, 0) → gpio_set_value(28, val) 注册为字符设备 /dev/fun_led write(\u0026ldquo;1\u0026rdquo;) = LED 亮，write(\u0026ldquo;0\u0026rdquo;) = LED 灭，read = 读当前状态 第 4 步：修改构建配置 文件 改动 common/drivers/misc/Kconfig 新增 CONFIG_FUN_LED（依赖 GPIOLIB） common/drivers/misc/Makefile 新增 obj-$(CONFIG_FUN_LED) += fun_led.o common/common_drivers/arch/arm64/configs/amlogic_gki.fragment 新增 CONFIG_FUN_LED=m common/common_drivers/modules.bzl 新增 \u0026quot;drivers/misc/fun_led.ko\u0026quot; 第 5 步：编译 cd ~/android/aml/s905x5/aml-s905x5-androidu-v2 ./build.sh -kj200 确认产物：out/android14-5.15/dist/fun_led.ko\n第 6 步：烧录并测试 # 烧录后，在板子上 adb shell insmod /vendor/lib/modules/fun_led.ko dmesg | grep fun_led # 看 major 号 # 用获取到的 major 创建设备节点（假设 major=237） adb shell mknod /dev/fun_led c 237 0 # 控制 LED echo 1 \u0026gt; /dev/fun_led # LED 亮 echo 0 \u0026gt; /dev/fun_led # LED 灭 cat /dev/fun_led # 读取当前状态 # 验证：万用表测 GPIOD_4 引脚电平变化 # 卸载 adb shell rmmod fun_led 关键收获 1. 你实际完成了什么 最初的目标是\u0026quot;控制 GPIOD_4 (FUN_LED)\u0026quot;。最终发现不需要写任何驱动，内核的 leds-gpio 驱动已经通过 DTS 接管了该引脚，提供标准 sysfs 接口：\necho 1 \u0026gt; /sys/class/leds/sys_led_red/brightness # 亮 echo 0 \u0026gt; /sys/class/leds/sys_led_red/brightness # 灭 2. 核心认知 DTS 描述硬件，驱动不再硬编码 — 你的 fun_led.c 把 LED_GPIO 28 写死在 C 代码里，而 leds-gpio 通过 DTS 的 gpios = \u0026lt;\u0026amp;gpio GPIOD_4 GPIO_ACTIVE_LOW\u0026gt; 声明硬件连接，换板子只改 DTS gpiod consumer API 替代 legacy 整数 API — gpiod_get(dev, \u0026quot;led\u0026quot;, ...) 返回描述符，代替 gpio_request(28, ...) 内核设备模型统一管理 sysfs — 驱动只声明业务属性（dev_groups），通用文件（device/subsystem/uevent/power）由 device_add() 自动创建 3. fun_led.ko 为何失败 GPIOD_4 已被 DTS 中的 leds-gpio 驱动占用，重复申请导致 -517（-EPROBE_DEFER）。详见 debug.md L5-02。\n4. 延伸阅读 所有深度分析已整理至 level-5-总结与扩展.md。\n关键文件路径速查 设备树（DTS） 用途 路径 SoC 级 DTSI（GPIO 控制器 bank@4000） common/common_drivers/arch/arm64/boot/dts/amlogic/meson-s7d.dtsi 板级 DTS（gpio_leds { sys_led_red }） common/common_drivers/arch/arm64/boot/dts/amlogic/s7d_s905x5m_bm201.dts GPIO pin 编号宏（GPIOD_4 = 28） common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h GPIO / pinctrl 驱动 用途 路径 S7D pinctrl 驱动（bank 表、pinmux） common/common_drivers/drivers/gpio/pinctrl/pinctrl-meson-s7d.c Meson pinctrl 通用驱动（meson_get_bank、meson_calc_reg_and_bit） common/drivers/pinctrl/meson/pinctrl-meson.c gpiolib 核心（gpio_to_desc、gpiod_request） common/drivers/gpio/gpiolib.c gpiolib OF 解析（of_get_named_gpiod_flags、of_gpio_simple_xlate） common/drivers/gpio/gpiolib-of.c pinctrl 核心（pinctrl_gpio_request） common/drivers/pinctrl/core.c LED 驱动与 sysfs 用途 路径 leds-gpio 驱动（compatible = \u0026quot;gpio-leds\u0026quot;） common/drivers/leds/leds-gpio.c LED class 核心（sysfs 属性注册） common/drivers/leds/led-class.c LED core 辅助（led_init_default_state_get） common/drivers/leds/led-core.c fwnode 属性读取（fwnode_property_read_string） common/drivers/base/property.c 设备模型核心（device_add、sysfs 创建） common/drivers/base/core.c 电源管理 sysfs（power/ 目录） common/drivers/base/power/sysfs.c fun_led.c（实验用硬编码驱动） 用途 路径 板子上的 ko /vendor/lib/modules/fun_led.ko 内核树源码 common/drivers/misc/fun_led.c 本地实验参考 learn/experiments/fun_led.c 构建配置 用途 路径 Kconfig common/drivers/misc/Kconfig Makefile common/drivers/misc/Makefile Bazel fragment（实际生效） common/common_drivers/arch/arm64/configs/amlogic_gki.fragment Bazel 模块拷贝列表 common/common_drivers/modules.bzl 构建脚本 ./build.sh（工作区根目录） 板子上用户态路径 用途 命令/路径 LED 控制 echo 1 \u0026gt; /sys/class/leds/sys_led_red/brightness GPIO 信息 gpioinfo / gpiodetect / gpioset / gpioget debugfs GPIO 列表 /sys/kernel/debug/gpio（需先挂载 debugfs） gpiochip /dev/gpiochip0 字符设备 /dev/fun_led（方式 B 创建） 验收清单 项目 验证方式 找到板子的 GPIO controller 通过 gpioinfo 或 /sys/kernel/debug/gpio 确认 控制电平 万用表/逻辑分析仪测量到引脚电平变化 理解设备树 你能在 DTS 中找到 GPIO controller 节点，说出 reg 和 gpio-controller 的含义 ioremap 你能回答\u0026quot;为什么不直接访问物理地址\u0026quot; ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-5-gpio/","summary":"\u003ch1 id=\"level-5操作-gpio\"\u003eLevel 5：操作 GPIO\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e控制板子上的一个物理引脚，让它输出高/低电平，用万用表或 LED 确认电平变化。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eGPIO 在系统里是以什么方式呈现的？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e物理上：芯片的一个管脚，可以设置输出高/低电平或读取输入电平\u003c/li\u003e\n\u003cli\u003e代码上：GPIO controller 是一个硬件模块，通过寄存器操作\u003c/li\u003e\n\u003cli\u003e给用户的接口：sysfs 或 libgpiod，或者自己写驱动操作寄存器\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e设备树（DTS）和驱动是什么关系？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDTS 描述硬件有什么、接在哪个总线、地址/中断号是什么\u003c/li\u003e\n\u003cli\u003e驱动根据 DTS 的信息来初始化硬件\u003c/li\u003e\n\u003cli\u003e同一个驱动可以支持多个 DTS 描述的实例\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e你的板子的 GPIO 信息从哪儿来？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e原理图（PDF）——告诉你在板子上哪个排针/接口引出了哪个 GPIO\u003c/li\u003e\n\u003cli\u003eDTS——告诉内核哪个 GPIO controller 对应哪个基地址\u003c/li\u003e\n\u003cli\u003edatasheet——详细到每个寄存器的位定义\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eGPIO 子系统\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e内核用 gpiolib 统一管理 GPIO。用户态访问 GPIO 有两种方式：\u003c/p\u003e\n\u003cp\u003e旧方式（sysfs，kernel \u0026lt; 5.10）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#ae81ff\"\u003e100\u003c/span\u003e \u0026gt; /sys/class/gpio/export\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho out \u0026gt; /sys/class/gpio/gpio100/direction\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e \u0026gt; /sys/class/gpio/gpio100/value\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e新方式（libgpiod，kernel \u0026gt;= 5.10）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egpioinfo\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egpioset gpiochip0 100\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e你的内核是 5.15，优先用 libgpiod。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eioremap 的作用\u003c/strong\u003e\u003c/p\u003e","title":"Level 5：操作 GPIO"},{"content":"Level 4 总结与扩展 总结：一个字符驱动的完整生命周期 Level 4 的核心命题——\u0026ldquo;写一个内核模块，用户态能读能写\u0026rdquo;——拆解为三层：内核侧（驱动代码）→ 构建侧（编译系统）→ 运行侧（板子验证）。下面逐层复盘，标注优先级。\n第一层：内核侧 —— 驱动代码本身 这层是跨平台通用的 Linux 驱动知识，与 Android 构建系统无关。\n🔴 核心必懂\n知识点 一句话 关联的坑 file_operations 驱动的\u0026quot;函数表\u0026quot;——把用户态的系统调用（open/read/write）映射成驱动函数 多态机制，所有字符驱动公用 register_chrdev(0, name, \u0026amp;fops) 向内核注册字符设备，参数 0 让内核自动分配主设备号 返回值是主设备号，mknod 需要它 copy_from_user / copy_to_user 内核与用户空间之间的安全数据拷贝 不能直接用 memcpy——见下方说明 dev_read 必须返回 0 表示 EOF VFS 规范：当数据读完时必须返回 0，否则 cat 等工具无限循环 L4-05 bug：忘记返回 0，cat 死循环 module_init / module_exit 模块加载/卸载的入口和出口 卸载时未 unregister_chrdev 会导致空悬设备节点 为什么不能用 memcpy 替代 copy_to_user？ 三个原因：\n安全校验：copy_to_user 会检查用户态指针是否在当前进程的地址空间内，防止内核被伪造指针攻击 缺页处理：用户态内存可能被换出（swap），copy_to_user 能触发缺页中断把页面换回来，memcpy 做不到 SMAP/PAN 硬件保护：ARMv8.1+ 有 PAN（Privileged Access Never）特性，内核态默认禁止直接访问用户态内存，copy_to_user 会临时关闭此保护 VFS 调用链路（用户态 open(\u0026quot;/dev/simple_char\u0026quot;) → 驱动 dev_open()）：\n用户态 open() → sys_open() → VFS 层 → 从 inode 提取设备号 → chrdevs[major].fops-\u0026gt;open(inode, file) ← 这就是你的 dev_open() 🟡 了解即可\n__init / __exit 标记：让内核在模块初始化/卸载后回收函数占用的内存，对常驻内存的内建驱动有意义，对可加载模块（.ko）影响不大 MODULE_LICENSE(\u0026quot;GPL\u0026quot;)：不声明会导致内核标记模块为\u0026quot;污染\u0026quot;（tainted），但不会阻止加载 第二层：构建侧 —— 从源码到 .ko 这层有大量 Android/Amlogic 构建系统特有的知识，换一个项目可能需要重新理解。\n🔴 核心必懂\n知识点 一句话 关联的坑 Kconfig 条目 让 make menuconfig 或 fragment 能找到并启用 CONFIG_SIMPLE_CHAR drivers/misc/Kconfig 添加 config SIMPLE_CHAR + tristate Makefile 条目 obj-$(CONFIG_SIMPLE_CHAR) += simple_char.o，把编译条件连接到配置项 L4-01/L4-02：忘了加 → 编译时找不到目标 fragment 配置 在 amlogic_gki.fragment 中添加 CONFIG_SIMPLE_CHAR=m L4-03：改了错的 fragment 文件 两个 fragment 文件 Bazel 用的在 common_drivers/arch/arm64/configs/，传统 make 用的在 common/arch/arm64/configs/ 修改配置前先确认 build.config.amlogic.bazel 的引用路径 modules.bzl 注册 Bazel 编译后，必须在 AMLOGIC_COMMON_MODULES 列表中声明 .ko 才会被拷贝到 dist L4-04：编出来了但没拷贝 LLVM=1 交叉编译 项目无 GCC 工具链，内核用 Clang 编译，目标 triple 为 aarch64-linux-android31 vermagic 不匹配就是因为 make 和 Bazel 工具链不同 Bazel 构建流程图（你的 ./build.sh -k 实际做了什么）：\n./build.sh -k → BUILD.bazel 加载 common_drivers/amlogic.bzl → 读取 build.config.amlogic.bazel → defconfig: gki_defconfig（GKI 基础配置） → fragment: common_drivers/arch/arm64/configs/amlogic_gki.fragment（厂商叠加） → Bazel 在沙箱中执行 make LLVM=1 ...（不污染源码树） → 输出 .ko 到 out/bazel/ → modules.bzl 列出哪些 .ko 需要拷贝 → 最终产物：out/android14-5.15/dist/simple_char.ko 🟡 了解即可\nBazel 缓存机制：基于 content hash，输入不变则复用上次结果。改 .c 会自动检测，但改 fragment 有时需要清缓存（rm -rf out/bazel/） make mrproper：清理所有编译中间文件（.config、.o 等），Bazel 要求源码树绝对干净 vermagic 机制：内核模块加载时内核会检查模块的 vermagic 字符串（包含内核版本、编译器版本等），不一致则拒绝加载。这就是为什么用 make M=drivers/misc 编的 .ko 在板子上 insmod 失败 第三层：运行侧 —— 从 .ko 到用户态可用 这层连接\u0026quot;编译产物\u0026quot;和\u0026quot;板子上能跑\u0026quot;。\n🔴 核心必懂\n知识点 一句话 关联的坑 vendor_dlkm 分区 Android 厂商内核模块的专用分区，路径 /vendor/lib/modules/ 模块随镜像烧录自动部署 modules.load 自动加载 第一阶段的模块自动加载列表，每次开机内核自动 insmod 不在列表里的模块不会自动加载 mknod 创建设备节点 mknod /dev/simple_char c \u0026lt;major\u0026gt; 0，让用户态能通过路径访问设备 没有设备节点 → /dev/simple_char 不存在 → open 报 ENOENT chmod 666 允许非 root 用户读写设备，不加则 echo 可能报 Permission denied 644 只允许 root 写 cat /proc/devices 查看已加载设备的名称和主设备号 mknod 需要知道 major 号 模块自动加载的两级机制：\n第一级（开机时）：modules.load → vendor_dlkm 中的模块 为什么需要 modules.load?——————因为有些驱动没有自动探测能力/系统启动必须依赖 第二级（按需加载）：modules.alias → 设备树匹配时 udev 自动加载 举例：USB鼠标 你插入 USB 鼠标。 系统： 发现一个USB设备 设备信息： VID=046D PID=C077 内核： 查找谁支持它 然后： 自动加载 usbhid.ko simple_char.ko 在第一级 modules.load 中，因此刷机后自动加载，不需要手动 insmod。\n验证流程：\n1. ls /vendor/lib/modules/simple_char.ko → 确认模块已部署 2. cat /proc/devices | grep simple_char → 确认已加载 + 获取 major 3. mknod /dev/simple_char c \u0026lt;major\u0026gt; 0 → 创建设备节点（只需一次） 4. chmod 666 /dev/simple_char → 放开权限 5. echo \u0026#34;hi\u0026#34; \u0026gt; /dev/simple_char \u0026amp;\u0026amp; cat /dev/simple_char → 验证读写 6. dmesg | grep simple_char → 确认驱动函数被调用 扩展 驱动的三种存在形式 你写的 simple_char.c 最终编成了 simple_char.ko（内核模块），但这只是其中一种形式。Linux 驱动有三种存在方式：\n形式 配置符号 产物 加载时机 如何确认 内建（built-in） CONFIG_XXX=y 编入 vmlinux（内核本体） 内核启动时即存在 无法 lsmod 看到；/sys/module/xxx/ 可能存在 模块（module） CONFIG_XXX=m xxx.ko 文件 开机自动加载 或 手动 insmod lsmod 能看到；/vendor/lib/modules/xxx.ko 存在 不编译 # CONFIG_XXX is not set 无 — — 内建 vs 模块的实际区别：\n内建 (y)： vmlinux 二进制里就包含驱动代码 优点：不需要任何文件系统，内核一启动就能用 缺点：vmlinux 变大，占用内存，无法卸载 典型场景：串口控制台驱动（console= 需要尽早输出） 模块 (m)： 独立的 .ko 文件，存放在文件系统上 优点：按需加载/卸载，节省内存，方便调试替换 缺点：依赖文件系统挂载，需要 modules.load 或 udev 典型场景：WiFi 驱动、USB 设备驱动、你写的 simple_char 你的项目中如何区分：\n# 查看哪些驱动是内建的（编进了 vmlinux） grep \u0026#34;=y\u0026#34; common/arch/arm64/configs/amlogic_gki.fragment # 查看哪些是模块 grep \u0026#34;=m\u0026#34; common/arch/arm64/configs/amlogic_gki.fragment 为什么 simple_char 选 m 而不是 y？ 学习阶段用模块：改代码 → 重编 .ko → 推送到板子 → rmmod \u0026amp;\u0026amp; insmod，秒级迭代。如果选 y，每次改驱动都要重编整个内核并重烧镜像，至少 10 分钟。\n驱动和设备的对应关系 你写的 simple_char 很特殊——它是纯软件驱动，不控制任何硬件。但真正的驱动（GPIO、I2C、SPI、USB 等）需要解决一个问题：内核如何知道\u0026quot;这个驱动\u0026quot;负责\u0026quot;这个设备\u0026quot;？\n这就是 驱动-设备匹配 机制。\n总线模型（Bus-Driver-Device） Linux 把驱动和设备组织在总线上：\n总线（bus）—— 平台总线 / I2C 总线 / SPI 总线 / USB 总线 / PCI 总线 ├── 驱动侧（driver）—— 声明\u0026#34;我能驱动哪些设备\u0026#34; └── 设备侧（device） —— 声明\u0026#34;我是谁\u0026#34;（设备树节点 / ACPI 表 / 动态枚举） 总线负责对号入座——当设备出现时，遍历已注册的驱动，找到匹配的就调用驱动的 probe() 函数。\n对比你的 simple_char：\nsimple_char 典型硬件驱动（如 GPIO） 注册方式 register_chrdev() 直接注册 先向总线注册 driver，匹配成功才 probe() 设备来源 纯软件，没有对应硬件 设备树（/devicetree）中的节点 匹配方式 无匹配——创建设备节点就可用 compatible 字符串匹配（见下文） probe() 无 有，匹配后初始化硬件 三种常见的匹配方式 方式一：设备树 compatible 匹配（你板子上的主流方式）\n你的板子（S905X5M）的设备树中有这样的节点：\n// 示例：arch/arm64/boot/dts/amlogic/g12a_s905x2.dts 中的串口 serial@24000 { compatible = \u0026#34;amlogic,meson-gx-uart\u0026#34;; // ← 这是\u0026#34;我是谁\u0026#34;的声明 reg = \u0026lt;0x24000 0x18\u0026gt;; interrupts = \u0026lt;...\u0026gt;; }; 驱动代码中：\n// drivers/tty/serial/meson_uart.c static const struct of_device_id meson_uart_dt_match[] = { { .compatible = \u0026#34;amlogic,meson-gx-uart\u0026#34; }, // ← \u0026#34;我能驱动这个\u0026#34; { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, meson_uart_dt_match); static struct platform_driver meson_uart_driver = { .probe = meson_uart_probe, // 匹配成功 → 调用此函数 .driver = { .name = \u0026#34;meson_uart\u0026#34;, .of_match_table = meson_uart_dt_match, }, }; 匹配流程： 内核解析设备树 → 创建 platform_device → 扫描已注册的 platform_driver → compatible 字符串一致 → 调用 .probe()\n方式二：设备 ID 表（USB/PCI 设备）\nUSB 鼠标插入 → 内核读取设备描述符：VID=046D, PID=C077（罗技） → 扫描 USB 驱动中 usb_device_id 表 → usbhid 驱动声明了 USB_DEVICE(0x046D, 0xC077) → 匹配！→ 调用 usbhid_driver.probe() 这对应扩展前总结中提到的 modules.alias 第二级加载机制。\n方式三：设备名称匹配（platform_driver 的 .name 回退）\n// 驱动侧 static struct platform_driver my_driver = { .driver = { .name = \u0026#34;my_device\u0026#34;, // ← 名称 }, }; // 设备侧（在板级文件中静态声明，老式方法） static struct platform_device my_device = { .name = \u0026#34;my_device\u0026#34;, // ← 名称一致 → 匹配 }; 有设备树后这种方式逐渐被 compatible 方式取代，但 .name 字段在 platform_driver_register() 时的 sysfs 目录命名中仍然重要。\n回到你的 simple_char：为什么不需要匹配？ simple_char 用 register_chrdev() 注册，走的是最古老的字符设备接口：\nregister_chrdev() → 在 chrdevs[] 数组中占据一个 major → 返回 major 号 用户态 mknod /dev/simple_char c \u0026lt;major\u0026gt; 0 → 在文件系统中创建入口 用户态 open(\u0026#34;/dev/simple_char\u0026#34;) → VFS 查 chrdevs[major] → 找到 fops 这条路径不经过总线、不经过 probe、不经过设备树。它是一个完全由用户态驱动的\u0026quot;虚拟设备\u0026quot;——设备节点存在即可使用，没有对应的硬件。\n你的 Level 5 中会涉及真正硬件驱动的写法，届时会看到 platform_driver、compatible 匹配、probe() 函数、设备树节点这些概念的实际使用。\n三类 dlkm (动态独立内核模块)分区：GKI 架构下的内核模块隔离 Android 的 GKI（Generic Kernel Image）架构把内核模块拆到了三个独立分区。你的编译产物中存在三个 _dlkm 目录：\nout/target/product/ross/ ├── system_dlkm/ ← GKI 通用模块（Google 维护） ├── vendor_dlkm/ ← Amlogic 厂商模块 └── odm_dlkm/ ← ODM 设备品牌定制模块（本项目中为空） 各自的内容和来源 分区 模块来源 板子路径 数量 典型内容 system_dlkm common/（GKI 上游内核） /system/lib/modules/\u0026lt;kernel-version\u0026gt;/ 16 个（你项目） USB 网卡、PPP、Bluetooth、CAN、zram vendor_dlkm common_drivers/（Amlogic 厂商驱动） /vendor/lib/modules/ 128 个（你项目） Amlogic GPIO/I2C/SPI/CLK/DRM/thermal/simple_char odm_dlkm ODM 自己的驱动源码 /odm/lib/modules/ 0 个（你的项目不需要） 屏幕触摸校准、特殊按键映射等 你的 simple_char.ko 在 vendor_dlkm 中，因为它在 modules.bzl 的 AMLOGIC_COMMON_MODULES 列表里，这个列表对应 vendor_dlkm 分区的模块。\nsystem_dlkm vs vendor_dlkm 的细微差异 除了源码来源不同，路径结构也有区别：\n# system_dlkm —— 带内核版本号，保留 kernel 源码路径层级 /system/lib/modules/5.15.170-android14-11-g3efc8295014d-ab12916019/ kernel/drivers/net/usb/usbnet.ko # vendor_dlkm —— 不带版本号，扁平化存放 /vendor/lib/modules/ simple_char.ko system_dlkm 带完整的内核版本号路径，是为了支持多个内核版本共存——Google 可以用同一个 system 镜像配合不同版本的内核。vendor_dlkm 由 Amlogic 随 BSP 一起发布，内核版本固定，不需要版本化路径。\n为什么分成三个分区——独立 OTA 更新 这是 GKI 架构的核心价值所在：\nGoogle 更新 Linux 内核 CVE 漏洞： 只需推送 system_dlkm 分区，不动 vendor_dlkm 和 odm_dlkm Amlogic 更新 HDMI 驱动 bug： 只需推送 vendor_dlkm 分区，不动 system_dlkm 设备品牌修改屏幕参数： 只需推送 odm_dlkm 分区，不动前两个 三个分区各自独立签名、独立更新，互不干扰。这解决了旧 Android 的毒瘤——\u0026ldquo;内核一锅粥\u0026rdquo;：vendor 改了内核源码，Google 的安全补丁就合不进去；Google 更新了内核，vendor 的驱动就挂。GKI 通过接口冻结（KMI，Kernel Module Interface）保证 system_dlkm 模块升级后，vendor_dlkm 模块不会因为符号表变化而加载失败。\n和你学过的 modules.load 的对应关系 每个分区有自己的 modules.load，由各自的 init 阶段按顺序加载：\n开机启动顺序： 1. system_dlkm 模块 → /system/lib/modules/\u0026lt;ver\u0026gt;/modules.load 2. vendor_dlkm 模块 → /vendor/lib/modules/modules.load ← simple_char.ko 在这里 3. odm_dlkm 模块 → /odm/lib/modules/modules.load 这解释了为什么 simple_char.ko 在 vendor_dlkm/lib/modules/modules.load 中——它是 vendor_dlkm 分区的第一级加载列表，加载时机在 system_dlkm 之后。\n一个驱动对应多个设备 你的 simple_char 是 一对一 关系——一个驱动管理一个设备。但现实中的驱动通常是 一对多：一个 LED 驱动同时控制板子上的 3 个 LED，一个 GPIO 驱动管理 32 路 GPIO。\n下面用酒店比喻从头到尾解释一对多的完整机制。目标是让你看懂每一行代码在\u0026quot;真实世界\u0026quot;中对应什么。\n整体类比 酒店集团（驱动）开分店 → 分店1 在 上海（LED 红灯） → 分店2 在 北京（LED 绿灯） → 分店3 在 深圳（LED 蓝灯） 概念映射表：\n概念 酒店比喻 代码中的东西 酒店集团 = 驱动本身 \u0026ldquo;simple_led 酒店集团\u0026rdquo; platform_driver 结构体 分店地址编号 = 设备号 上海=251.00, 北京=251.01, 深圳=251.02 dev_t (major + minor) 分店名 = 设备节点 上海分店, 北京分店\u0026hellip; /dev/simple_led_0 共享服务标准 = fops 所有分店共用同一套\u0026quot;客房服务流程\u0026quot; file_operations 分店选址清单 = 设备树 工商局的\u0026quot;待建分店规划\u0026quot; device tree 节点 第一步：酒店集团注册领执照 你要开连锁酒店 → 先去工商局注册公司 → 拿到营业执照号 代码中的 alloc_chrdev_region：\nstatic dev_t dev_num; // 营业执照号 /* 向内核（工商局）申请一段连续的设备号 */ alloc_chrdev_region(\u0026amp;dev_num, 0, MAX_LEDS, \u0026#34;simple_led\u0026#34;); /* ↑ ↑ ↑ ↑ ↑ * \u0026#34;帮我分配\u0026#34; 执照号 从01 最多16 公司名 */ 结果：拿到了营业执照号 major=251（主设备号）。内核知道\u0026quot;有一个叫 simple_led 的公司，它的执照号范围是 251.00 ~ 251.15\u0026quot;。\n但是此时还没开任何店，也没招员工。\n第二步：注册连锁品牌 拿到了营业执照 → 还要注册品牌名 → 方便客人通过品牌名找到分店 代码中的 class_create：\nled_class = class_create(THIS_MODULE, \u0026#34;simple_led\u0026#34;); 效果：内核在 /sys/class/ 下创建了一个 simple_led 目录。\n/sys/class/simple_led/ ← 品牌名注册处 这个 class 的作用是：后面你每开一家分店（device_create），它会自动在 /sys/class/simple_led/ 下创建一个条目，udev（内核的\u0026quot;物业系统\u0026quot;）看到后自动去 /dev/ 下创建设备节点。\n没有 class，你得手动 mknod。有 class，系统自动生成。\n第三步：声明\u0026quot;我能管理什么类型的店\u0026quot; 酒店集团发布公告：「我们能管理任何位于 上海/北京/深圳 的地段」 代码中的 of_device_id 匹配表：\n/* 酒店集团的\u0026#34;选址标准\u0026#34;——什么样的地段我会去开店 */ static const struct of_device_id led_dt_match[] = { { .compatible = \u0026#34;simple,led-driver\u0026#34; }, /* ← 我只看地块上写没写这句话 */ { } /* 表结束标记 */ }; 然后把它装进 platform_driver（酒店管理公司资质）：\nstatic struct platform_driver led_driver = { .probe = led_probe, /* 选好址后，派我去管理这家店 */ .driver = { .name = \u0026#34;simple_led\u0026#34;, .of_match_table = led_dt_match, /* ← 我的选址标准 */ }, }; platform_driver_register(\u0026amp;led_driver) —— 向\u0026quot;建设局（内核）\u0026ldquo;注册。内核（建设局）收到注册后，立刻翻开\u0026quot;在建/已建项目清单\u0026rdquo;（设备树），找所有写着 compatible = \u0026quot;simple,led-driver\u0026quot; 的地块。每找到一个，就派 led_probe() 去一次。\n第四步：设备树——建设局的\u0026quot;待建清单\u0026quot; 设备树就是政府的土地规划图，上面画好了每一块地要建什么：\nleds { compatible = \u0026#34;simple,led-driver\u0026#34;; /* ← 这块地皮属于 simple_led 集团 */ led_red { label = \u0026#34;red\u0026#34;; gpios = \u0026lt;\u0026amp;gpio 15 0\u0026gt;; /* 建在地块 15 上 */ }; led_green { label = \u0026#34;green\u0026#34;; gpios = \u0026lt;\u0026amp;gpio 16 0\u0026gt;; /* 建在地块 16 上 */ }; led_blue { label = \u0026#34;blue\u0026#34;; gpios = \u0026lt;\u0026amp;gpio 17 0\u0026gt;; /* 建在地块 17 上 */ }; }; 内核启动时解析设备树，为每个子节点创建一个 platform_device（相当于\u0026quot;建设用地批复文件\u0026quot;），然后等着匹配驱动。\n至此，所有准备工作完成。下面发生的事就是一切自动串联起来的。\n第五步：probe —— 派店长去管理每一家分店 当 platform_driver_register 执行后，建设局（内核）的动作：\n建设局翻开清单： → 找到 led_red: 地块上写着 \u0026#34;simple,led-driver\u0026#34; ✓ → 派 led_probe() → 找到 led_green:地块上写着 \u0026#34;simple,led-driver\u0026#34; ✓ → 派 led_probe() → 找到 led_blue: 地块上写着 \u0026#34;simple,led-driver\u0026#34; ✓ → 派 led_probe() led_probe() 被调用了 三次，每次进来时 pdev 指向不同的地块：\n/* 第一次进来：pdev = led_red * 第二次进来：pdev = led_green * 第三次进来：pdev = led_blue */ static int led_probe(struct platform_device *pdev) { probe 是\u0026quot;店长\u0026quot;，它负责：\n给这家店分配编号（minor）——你是第几家开的分店 租地建店（获取 GPIO）——这块地（GPIO 引脚）归我管 挂上招牌（device_create → /dev/simple_led_N）——客人能找到了 #include \u0026lt;linux/module.h\u0026gt; #include \u0026lt;linux/platform_device.h\u0026gt; #include \u0026lt;linux/gpio/consumer.h\u0026gt; #include \u0026lt;linux/cdev.h\u0026gt; #include \u0026lt;linux/fs.h\u0026gt; #define DEVICE_NAME \u0026#34;simple_led\u0026#34; #define MAX_LEDS 16 static dev_t dev_num; /* 营业执照号（第一次分配到的起始设备号） */ static struct class *led_class; /* 品牌名（/sys/class/simple_led/） */ static int led_count; /* 记录开了几家分店，用作次设备号索引 */ /* ---------- 每家分店的私有数据 ---------- */ struct led_device { struct cdev cdev; /* 这家店的\u0026#34;营业许可证\u0026#34; */ struct gpio_desc *gpio; /* 这家店占用的地块（GPIO） */ int minor; /* 分店编号（次设备号） */ bool state; /* 灯当前亮灭 */ }; /* ---------- probe：每一次调用 = 开一家新分店 ---------- */ static int led_probe(struct platform_device *pdev) { struct led_device *led; struct device *dev = \u0026amp;pdev-\u0026gt;dev; /* 这家店的规划文件 */ dev_t devt; /* 1. 租场地——从设备树取 GPIO */ led-\u0026gt;gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW); /* \u0026#34;这块地（GPIO）归我管了，默认输出低电平（灯灭）\u0026#34; */ /* 2. 分店编号：第一家拿 0，第二家拿 1，第三家拿 2 */ led-\u0026gt;minor = led_count++; /* 3. 办营业许可证 + 挂招牌 */ devt = MKDEV(MAJOR(dev_num), led-\u0026gt;minor); /* 分店完整地址：251.0, 251.1, 251.2 */ cdev_init(\u0026amp;led-\u0026gt;cdev, \u0026amp;led_fops); /* 制作\u0026#34;服务菜单\u0026#34; */ cdev_add(\u0026amp;led-\u0026gt;cdev, devt, 1); /* 正式取得营业许可 */ device_create(led_class, dev, devt, NULL, \u0026#34;simple_led_%d\u0026#34;, led-\u0026gt;minor); /* 挂招牌 → 自动生成 /dev/simple_led_N */ dev_info(dev, \u0026#34;LED %d probed\\n\u0026#34;, led-\u0026gt;minor); return 0; } 三次 probe 完成后，局面：\n/dev/simple_led_0 ← 次设备号=0，控制 GPIO 15（red） /dev/simple_led_1 ← 次设备号=1，控制 GPIO 16（green） /dev/simple_led_2 ← 次设备号=2，控制 GPIO 17（blue） 三个 /dev/ 节点，共用同一套 led_fops。\nfops 声明和读写实现： static ssize_t led_read(struct file *filep, char __user *buf, size_t len, loff_t *off) { /* filep-\u0026gt;private_data — \u0026#34;你的房卡\u0026#34;上的分店标记 */ struct led_device *led = filep-\u0026gt;private_data; char val = led-\u0026gt;state ? \u0026#39;1\u0026#39; : \u0026#39;0\u0026#39;; if (*off \u0026gt;= 1) return 0; if (copy_to_user(buf, \u0026amp;val, 1)) return -EFAULT; *off += 1; return 1; } static ssize_t led_write(struct file *filep, const char __user *buf, size_t len, loff_t *off) { struct led_device *led = filep-\u0026gt;private_data; char val; if (copy_from_user(\u0026amp;val, buf, 1)) return -EFAULT; led-\u0026gt;state = (val == \u0026#39;1\u0026#39;); gpiod_set_value(led-\u0026gt;gpio, led-\u0026gt;state); /* 只操作这一个 LED 的 GPIO */ return len; } static struct file_operations led_fops = { .owner = THIS_MODULE, .read = led_read, .write = led_write, }; 第六步：用户来住店 —— private_data 是怎么连上的 用户态执行：\necho 1 \u0026gt; /dev/simple_led_0 # 点亮红灯 内核内部流程：\n用户 open(\u0026#34;/dev/simple_led_0\u0026#34;) → VFS 从文件名查到 inode → 从 inode 提取设备号 (251, 0) → 从字符设备表找到 cdev（= red 那份） → 从 cdev 找到 fops（所有店共用 led_fops） → 调用 open 实现（把 filep-\u0026gt;private_data 指向 red 的 led_device） → 返回 fd = 3 给用户 用户 write(fd, \u0026#34;1\u0026#34;, 1) → sys_write() → 通过 fd 找到 file 结构体 → file-\u0026gt;private_data → 指向了前面存好的 led_device（red，GPIO 15） → 调用 led_write()： struct led_device *led = filep-\u0026gt;private_data; // = red 那个实例 gpiod_set_value(led-\u0026gt;gpio, 1); // 只操作 GPIO 15，不动其他灯 private_data 的桥梁作用：\nprobe 时：创建 led_device（red, gpio=15）→ 记在内存里 open 时：根据次设备号找到对应的 led_device → 塞进 filep-\u0026gt;private_data write 时：从 private_data 取出 led_device → 操作它的 GPIO 每个进程打开的文件拥有独立的 file 结构体，所以多个进程同时操作不同的 LED，各自的 private_data 指向不同的 LED 实例，不会搞混。\n第七步：每个概念的关键代码映射 // 1. 领营业执照（分配设备号范围） alloc_chrdev_region(\u0026amp;dev_num, 0, MAX_LEDS, \u0026#34;simple_led\u0026#34;); // 2. 注册品牌（创建设备类，使自动生成 /dev/ 节点成为可能） led_class = class_create(THIS_MODULE, DEVICE_NAME); // 3. 声明选址标准（告诉内核我的匹配规则） static const struct of_device_id led_dt_match[] = { { .compatible = \u0026#34;simple,led-driver\u0026#34; }, { } }; MODULE_DEVICE_TABLE(of, led_dt_match); // 4. 向建设局注册触发匹配 static struct platform_driver led_driver = { .probe = led_probe, // 配上一个被调用三次的函数 .driver = { .of_match_table = led_dt_match, }, }; platform_driver_register(\u0026amp;led_driver); // → 内核立刻扫设备树，匹配就 probe // ===== 以上：准备工作 ===== // ===== 以下：probe 里的事，每个匹配的设备分支自动执行一次 ===== // 5. 租场地（申请 GPIO） led-\u0026gt;gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW); // 6. 分店编号（分配 minor） led-\u0026gt;minor = led_count++; // 7. 正式营业（cdev_add + device_create） devt = MKDEV(MAJOR(dev_num), led-\u0026gt;minor); cdev_init(\u0026amp;led-\u0026gt;cdev, \u0026amp;led_fops); cdev_add(\u0026amp;led-\u0026gt;cdev, devt, 1); device_create(led_class, dev, devt, NULL, \u0026#34;simple_led_%d\u0026#34;, led-\u0026gt;minor); 第八步：对比 simple_char 和你以前见过的代码 对比项 simple_char（你写的） 本页的 LED 驱动（现代标准写法） 注册函数 register_chrdev(0, ...) 全范围注册（旧 API） alloc_chrdev_region() + cdev_add()（精确控制） 设备实例数 1 个，hardcode 在源码里 每匹配一个设备树节点自动创建一个 区分实例 无（只有一个） filep-\u0026gt;private_data 设备节点 手动 mknod class_create + device_create 自动生成 和硬件关系 纯虚拟，无硬件 gpiod_set_value() 操作真实 GPIO 和设备树关系 无关 of_match_table + probe() 配对 register_chrdev 是 Linux 2.6 之前的遗留接口，现代驱动都用 cdev_add + device_create——你 Level 5 中做 GPIO 驱动时会直接使用后者的模式。\n一篇写给自己的话： 当初学这一节时最困惑的是——为什么这么复杂？register_chrdev 一行能搞定的事情，为什么要拆成 alloc_chrdev_region + class_create + cdev_init + cdev_add + device_create 五步？答案在于场景：register_chrdev 适合\u0026quot;一个驱动只管一个设备\u0026quot;的玩具代码，真正产品级的驱动需要管理多个设备实例（多个 LED、多个 GPIO bank），每一步拆开就是为了精确控制每一个实例的生命周期。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-4-%E6%80%BB%E7%BB%93%E4%B8%8E%E6%89%A9%E5%B1%95/","summary":"\u003ch1 id=\"level-4-总结与扩展\"\u003eLevel 4 总结与扩展\u003c/h1\u003e\n\u003ch2 id=\"总结一个字符驱动的完整生命周期\"\u003e总结：一个字符驱动的完整生命周期\u003c/h2\u003e\n\u003cp\u003eLevel 4 的核心命题——\u0026ldquo;写一个内核模块，用户态能读能写\u0026rdquo;——拆解为三层：\u003cstrong\u003e内核侧（驱动代码）→ 构建侧（编译系统）→ 运行侧（板子验证）\u003c/strong\u003e。下面逐层复盘，标注优先级。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"第一层内核侧--驱动代码本身\"\u003e第一层：内核侧 —— 驱动代码本身\u003c/h3\u003e\n\u003cp\u003e这层是\u003cstrong\u003e跨平台通用\u003c/strong\u003e的 Linux 驱动知识，与 Android 构建系统无关。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e🔴 核心必懂\u003c/strong\u003e\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e知识点\u003c/th\u003e\n          \u003cth\u003e一句话\u003c/th\u003e\n          \u003cth\u003e关联的坑\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003efile_operations\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e驱动的\u0026quot;函数表\u0026quot;——把用户态的系统调用（open/read/write）映射成驱动函数\u003c/td\u003e\n          \u003ctd\u003e多态机制，所有字符驱动公用\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eregister_chrdev(0, name, \u0026amp;fops)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e向内核注册字符设备，参数 0 让内核自动分配主设备号\u003c/td\u003e\n          \u003ctd\u003e返回值是主设备号，mknod 需要它\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003ecopy_from_user\u003c/code\u003e / \u003ccode\u003ecopy_to_user\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e内核与用户空间之间的安全数据拷贝\u003c/td\u003e\n          \u003ctd\u003e不能直接用 \u003ccode\u003ememcpy\u003c/code\u003e——见下方说明\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edev_read\u003c/code\u003e 必须返回 0 表示 EOF\u003c/td\u003e\n          \u003ctd\u003eVFS 规范：当数据读完时必须返回 \u003ccode\u003e0\u003c/code\u003e，否则 \u003ccode\u003ecat\u003c/code\u003e 等工具无限循环\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eL4-05 bug\u003c/strong\u003e：忘记返回 0，cat 死循环\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003emodule_init\u003c/code\u003e / \u003ccode\u003emodule_exit\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e模块加载/卸载的入口和出口\u003c/td\u003e\n          \u003ctd\u003e卸载时未 \u003ccode\u003eunregister_chrdev\u003c/code\u003e 会导致空悬设备节点\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e为什么不能用 \u003ccode\u003ememcpy\u003c/code\u003e 替代 \u003ccode\u003ecopy_to_user\u003c/code\u003e？\u003c/strong\u003e 三个原因：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e安全校验\u003c/strong\u003e：\u003ccode\u003ecopy_to_user\u003c/code\u003e 会检查用户态指针是否在当前进程的地址空间内，防止内核被伪造指针攻击\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e缺页处理\u003c/strong\u003e：用户态内存可能被换出（swap），\u003ccode\u003ecopy_to_user\u003c/code\u003e 能触发缺页中断把页面换回来，\u003ccode\u003ememcpy\u003c/code\u003e 做不到\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSMAP/PAN 硬件保护\u003c/strong\u003e：ARMv8.1+ 有 PAN（Privileged Access Never）特性，内核态默认禁止直接访问用户态内存，\u003ccode\u003ecopy_to_user\u003c/code\u003e 会临时关闭此保护\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eVFS 调用链路\u003c/strong\u003e（用户态 \u003ccode\u003eopen(\u0026quot;/dev/simple_char\u0026quot;)\u003c/code\u003e → 驱动 \u003ccode\u003edev_open()\u003c/code\u003e）：\u003c/p\u003e","title":"Level 4 总结与扩展"},{"content":"Level 4：写一个字符设备驱动 我想做 在内核里加一个最小内核模块，用户态 echo \u0026quot;hi\u0026quot; \u0026gt; /dev/simple_char 能写进去，cat /dev/simple_char 能读出来。\n先回答三个问题 内核驱动和用户态程序的根本区别是什么？\n驱动运行在内核态，可以访问所有硬件资源和内存 用户态程序通过系统调用（open/read/write/ioctl）请求内核代为操作 内核态出错 → 整个系统崩溃（kernel panic）；用户态出错 → 只崩那个进程 Linux 设备驱动分哪几类？\n字符设备──像文件一样 open/read/write/close，顺序访问。例子：串口（/dev/ttyAML0）、GPIO、I2C、SPI、按键。你的板子上 ls -l /dev/ 看到前面是 c 的都是字符设备 块设备──按块随机读写，有缓存层。例子：eMMC（/dev/block/mmcblk0）、U 盘、SD 卡。前面是 b 网络设备──通过 socket 接口访问，不通过文件系统。例子：以太网（eth0）、WiFi（wlan0）。ip link show 看到的就是 你的板子上跑 adb shell ls -l /dev/ | head -20，看看哪些是字符设备（c）、哪些是块设备（b）\n什么是 file_operations？\n它是一个结构体，里面全是函数指针 每个字符驱动需要告诉内核：\u0026ldquo;当用户调用 open() 时，执行我这个函数；调用 read() 时，执行我那个函数\u0026rdquo; 这是一个多态机制——不同的驱动实现相同的接口，用户态不用关心底层实现 需要懂的知识 驱动模块的骨架\n每个内核模块都有两个必经的函数：\nmodule_init(mod_init); // insmod 时调用 module_exit(mod_exit); // rmmod 时调用 __init 和 __exit 标记：这些函数在运行完后可以释放内存。\nregister_chrdev 做了什么\nmajor = register_chrdev(0, \u0026#34;simple_char\u0026#34;, \u0026amp;fops); 向内核注册一个字符设备 第一个参数 0 表示让内核自动分配主设备号 返回的 major 就是你设备的编号，cat /proc/devices 能看到它 mknod /dev/simple_char c \u0026lt;major\u0026gt; 0 就是创建一个指向这个编号的设备节点 为什么用 copy_to_user 而不是 memcpy\n内核不能直接访问用户态传过来的指针，原因：\n安全——用户态指针可能是伪造的，指向内核敏感数据 分页——用户态页面可能不在内存中（被换出了），直接访问会触发缺页 权限——内核必须显式地\u0026quot;拷入/拷出\u0026quot;用户空间 copy_from_user(kernel_buffer, user_buffer, len); copy_to_user(user_buffer, kernel_buffer, len); 这两个函数会检查指针合法性、处理缺页、返回未拷贝成功的字节数（0 表示全部成功）。\nKconfig 和 Makefile 的角色\nKconfig——定义编译选项，make menuconfig 的界面就从这里生成 Makefile——告诉 build 系统这个文件要编成模块（obj-m）还是编进内核（obj-y） 为什么放在 drivers/misc/\n内核源代码树的 drivers/ 下按功能划分子目录：\n子系统 目录 举例 GPIO drivers/gpio/ GPIO 控制器 I2C drivers/i2c/ I2C 总线及从设备 输入设备 drivers/input/ 按键、触摸屏 串口 drivers/tty/ UART 终端 杂项 drivers/misc/ 不属于以上子类的驱动 simple_char 不涉及任何具体硬件外设，没有对应的子系统归属，所以放在 drivers/misc/——内核的\u0026quot;其他\u0026quot;目录。如果写的是 I2C 温度传感器驱动，则应放到 drivers/i2c/ 以复用框架代码。\nVFS 层：用户 open → 驱动 dev_open 的完整链路\nLinux 内核 VFS（Virtual File System）是一个抽象层，让用户态可以像操作普通文件一样操作设备。从 open(\u0026quot;/dev/simple_char\u0026quot;, O_RDWR) 到 dev_open() 的完整流程分三步：\n用户态：int fd = open(\u0026#34;/dev/simple_char\u0026#34;, O_RDWR); │ ▼ 第一步：系统调用入口（内核接手） ┌────────────────────────────────────────────────────┐ │ sys_open() → 将用户态路径解析为 dentry + inode │ │ inode 中存有设备号：major = N, minor = 0 │ │ 发现这是一个\u0026#34;字符设备\u0026#34;（inode 有 S_ISCHR 标记） │ └────────────────────────────────────────────────────┘ │ ▼ 第二步：字符设备查找 ┌────────────────────────────────────────────────────┐ │ 内核从 inode 取出 major 号，到字符设备表 │ │ (cdev_map / chrdevs 数组) 中查找对应的 │ │ file_operations 结构体指针 │ │ │ │ 这是关键——你在 mod_init 里调用的 │ │ register_chrdev(0, \u0026#34;simple_char\u0026#34;, \u0026amp;fops) │ │ 就是往这张表里写了一条：major → \u0026amp;fops │ └────────────────────────────────────────────────────┘ │ ▼ 第三步：调用具体驱动函数 ┌────────────────────────────────────────────────────┐ │ VFS 把找到的 fops 指针填入当前进程的 fd 表中 │ │ 然后调用：fops-\u0026gt;open(inode, filep) │ │ 即你写的：dev_open() │ │ │ │ 之后 read/write 走同样的路径 │ │ sys_read() → 从 fd 找到 fops → fops-\u0026gt;read(...) │ └────────────────────────────────────────────────────┘ │ ▼ 返回用户态：得到 fd = 3 核心要点：\nVFS 是中间人——用户态只跟 VFS 打交道（系统调用），VFS 负责把请求路由到正确的驱动 注册就是建立映射——register_chrdev 的本质是在内核维护的字符设备表中登记 \u0026ldquo;major N → fops\u0026rdquo; inode 是桥梁——mknod 创建设备节点时，把 major/minor 写进了磁盘上的 inode；每次 open 时 VFS 从 inode 读设备号，再用设备号查 file_operations read/write 不再查路径——open 完成后，返回的 fd 已经直接关联到 fops，后续 read/write 通过 fd 直达驱动函数，不再走路径解析 正是因为这个设计，你才可以在用户态用标准的 open/read/write 操作任何字符设备，驱动代码写的 file_operations 和用户态之间完全不需要约定任何额外接口。\nARCH 和 CROSS_COMPILE：交叉编译的必要性\n你的开发机是 x86_64 架构（PC），但板子是 arm64（AArch64）架构。x86 的机器编译不出 arm64 的可执行文件——这是交叉编译的场景。\n但在你的环境中，内核模块编译不通过命令行传 CROSS_COMPILE\n实际上你的 AOSP 项目 没有 GCC 交叉编译工具链（prebuilts/gcc/ 下无 aarch64 目录），内核使用 LLVM/Clang 编译（build.config.common 中设了 LLVM=1）：\n# 查看内核构建配置中的关键设置 $ grep -rn \u0026#34;LLVM\\|CLANG_VERSION\\|NDK_TRIPLE\u0026#34; build.config.common build.config.constants build.config.common: LLVM=1 build.config.constants: CLANG_VERSION=r487747c build.config.constants: AARCH64_NDK_TRIPLE=aarch64-linux-android31 配置项 值 含义 LLVM=1 启用 使用 clang 而非 GCC 编译内核 CLANG_VERSION r487747c 使用的 clang 版本 AARCH64_NDK_TRIPLE aarch64-linux-android31 NDK 目标三元组（带 API level） 所以正确的编译方式有两种：\n方式一（推荐）：通过 AOSP 构建系统\nsource build/envsetup.sh lunch ross-userdebug # 进入内核目录自动有环境变量 cd common/common14-5.15/common make menuconfig make modules AOSP 的构建脚本会处理 clang 路径和 flags，不需要手动指定任何交叉编译参数。\n方式二（理解原理）：直接传给 make\nmake ARCH=arm64 LLVM=1 \\ CLANG_TRIPLE=aarch64-linux-android31- \\ CROSS_COMPILE=aarch64-linux-android31- \\ modules 此处 LLVM=1 让内核使用 clang 系列工具，CLANG_TRIPLE 和 CROSS_COMPILE 告诉内核目标平台是 aarch64-android（API 31）。注意这里的后缀是 aarch64-linux-android31-（带 API level），而非常见的 aarch64-linux-android-。\nGCC 方式 vs Clang/LLVM 方式：两种交叉编译哲学\n如果你之前在嵌入式项目（如 DVB Stack、机顶盒 SDK）中见过这样的配置：\nexport ALI_TOOLCHAIN=/path/to/arm-none-linux-gnueabihf 那是 GCC 式交叉编译。和 Android 使用的 Clang/LLVM 式 有本质区别：\nGCC 方式 Clang/LLVM 方式 编译器 arm-none-linux-gnueabihf-gcc clang --target=aarch64-linux-android31 架构范围 一个编译器只编一种架构 一个 clang 编所有架构 换架构 换编译器前缀（aarch64-linux-gnu-） 换 --target 参数 libc 捆绑在工具链目录里 --sysroot 在命令行指定，与编译器解耦 环境配置 shell 脚本 export 变量 build.config / Bazel 管理 多架构 每个架构一套完整的 binutils+gcc+libc 一套 Clang + 多份 sysroot sysroot 位置 藏在工具链目录内部 prebuilts/build-tools/sysroots/，显式独立 为什么 Android 选 Clang？ Android 一个源码树要同时编译 ARM32、ARM64、x86_64 三种架构（甚至更多）。如果用 GCC，需要维护三套完整的 binutils-gcc-glibc 工具链。Clang 则是一个编译器 + 三个 --target + 对应的 sysroot 即可——这是 LLVM 的\u0026quot;交叉编译即原生编译\u0026quot;设计哲学。\n为什么 DVB Stack 用 GCC？ 芯片厂商（如 Ali、MStar）提供的 SDK 通常锁定一个固定架构（arm-none-linux-gnueabihf），GCC 工具链和 glibc 随 SDK 一起发布，不涉及多架构切换。此时 GCC 的工具链 \u0026ldquo;开箱即用\u0026rdquo; 反而是优势。\n你的 simple_char.ko 和用户态测试程序编译，本质都是 Clang + \u0026ndash;target + sysroot 这条路径。\n为什么直接 make 不行——Bazel 构建系统\n你的 AOSP 项目已经切换到 Bazel 来编译内核（./build.sh -k 底层调用 bazel build），和传统 make 有本质区别：\nmake 直接在源码树编译 bazel 沙箱编译 源码树 留下 .o、.config 等中间文件 源码目录只读，不产生任何垃圾 缓存 靠文件时间戳判断 基于 content hash，输入不变就复用 配置 读 .config 从 build.config.* + defconfig/fragment 重新生成 .ko 产物 drivers/misc/simple_char.ko 输出到 out/bazel/ 下，不污染源码树 这解释了你实操中遇到的两个问题：\n源码树不干净——在 common/ 下敲 make 后留了中间文件，Bazel 检测到后拒绝编译（The source tree is not clean, please run 'make mrproper'）。修复：make mrproper 清理。\n改了 fragment 但 .config 没变——Bazel 缓存了旧的 config step，没检测到 fragment 变化。修复：清 Bazel 缓存或 rm -rf out/bazel/。\n内核配置的正确修改路径：\n修改 amlogic_gki.fragment → Bazel 合并 gki_defconfig + fragment → 生成 .config → 编译 你改这里 Bazel 自动完成 不要在 common/ 下直接执行 make 或 make menuconfig，它们会污染源码树且配置不会被 Bazel 采纳。\n动手方案 # 第 1 步：找到内核目录 ls ~/android/aml/s905x5/aml-s905x5-androidu-v2/common/ # 这个是内核源码目录（kernel 5.15） # 第 2 步：把驱动源码放进去 cat \u0026gt; ~/android/aml/s905x5/aml-s905x5-androidu-v2/common/common14-5.15/common/drivers/misc/simple_char.c \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; /* * simple_char — 最小字符设备驱动 * * 功能：用户态写入的数据暂存在内核缓冲区，读取时返回该数据。 * 相当于一个\u0026#34;内核版记事本\u0026#34;，用于演示 Linux 字符驱动骨架。 * * 测试方式： * echo \u0026#34;hello\u0026#34; \u0026gt; /dev/simple_char * cat /dev/simple_char */ /* module.h：模块的 module_init/module_exit/MODULE_LICENSE 等宏定义 */ #include \u0026lt;linux/module.h\u0026gt; /* kernel.h：printk 等内核打印函数 */ #include \u0026lt;linux/kernel.h\u0026gt; /* fs.h：register_chrdev、unregister_chrdev、file_operations 结构体 */ #include \u0026lt;linux/fs.h\u0026gt; /* uaccess.h：copy_to_user / copy_from_user —— 内核\u0026lt;-\u0026gt;用户空间数据拷贝 */ #include \u0026lt;linux/uaccess.h\u0026gt; /* 设备名称——注册时告诉内核，也用于 /proc/devices 显示和 mknod */ #define DEVICE_NAME \u0026#34;simple_char\u0026#34; /* major：内核分配的主设备号，mknod 时用来建立设备节点 */ static int major; /* * kernel_buffer：内核侧的存储缓冲区。 * 用户 write 的数据暂存于此，read 时从此读出。 * 简单驱动用固定大小；真实驱动需考虑并发、缓冲区溢出等问题。 */ static char kernel_buffer[256] = {0}; /* * dev_open — open() 系统调用的内核侧实现 * * inodep：文件在 VFS 层对应的 inode，用来获取设备信息 * filep：文件打开后在内核中的表示，每个进程的每次 open 对应一个实例 * * 返回 0 表示成功。大部分简单驱动只需要记录日志即可。 */ static int dev_open(struct inode *inodep, struct file *filep) { printk(KERN_INFO \u0026#34;simple_char: open()\\n\u0026#34;); return 0; } /* * dev_read — read() 系统调用的内核侧实现 * * buffer：用户态传入的缓冲区指针（带 __user 标记，不可直接解引用） * len：用户态请求读取的字节数 * offset：文件读写偏移位置，记录已读到的位置。读操作从这里继续，读完一段后向后移动 * * copy_to_user：将内核数据拷贝到用户空间 * - 返回值 0 表示全部拷贝成功 * - 返回值非零 = 未能拷贝的字节数 * * 返回规则： * 0 = EOF，表示已读到末尾，调用者（如 cat）收到 0 后会停止读循环并退出 * \u0026gt;0 = 本次实际读取的字节数 * \u0026lt;0 = 错误码 */ static ssize_t dev_read(struct file *filep, char __user *buffer, size_t len, loff_t *offset) { int data_len = strlen(kernel_buffer); /* 已读到数据末尾，返回 EOF */ if (*offset \u0026gt;= data_len) return 0; /* 限制读取长度不超过剩余数据 */ if (len \u0026gt; data_len - *offset) len = data_len - *offset; if (copy_to_user(buffer, kernel_buffer + *offset, len)) return -EFAULT; *offset += len; printk(KERN_INFO \u0026#34;simple_char: read() %zu bytes, offset=%lld\\n\u0026#34;, len, *offset); return len; } /* * dev_write — write() 系统调用的内核侧实现 * * buffer：用户态传入的数据指针（带 __user，需用 copy_from_user 访问） * len：用户态请求写入的字节数 * * copy_from_user：将用户空间数据拷贝到内核缓冲区 * - 检查用户指针合法性，处理缺页 * - 返回值 = 未能拷贝成功的字节数（0 = 全部成功） */ static ssize_t dev_write(struct file *filep, const char __user *buffer, size_t len, loff_t *offset) { int ret = copy_from_user(kernel_buffer, buffer, len); printk(KERN_INFO \u0026#34;simple_char: write() %zu bytes\\n\u0026#34;, len); return ret ? -EFAULT : len; } /* * file_operations 结构体： * 字符驱动的核心——将用户态系统调用（open/read/write/ioctl/release...） * 映射到驱动中的具体实现函数。这是 Linux 驱动多态机制的体现。 * * 未被赋值的成员（如 release/ioctl）内核会使用默认行为。 */ static struct file_operations fops = { .open = dev_open, .read = dev_read, .write = dev_write, }; /* * mod_init — 模块入口（insmod 时执行） * __init 标记：此函数运行结束后内核可回收其内存 * * register_chrdev(0, DEVICE_NAME, \u0026amp;fops)： * - 参数 0 表示让内核自动分配主设备号 * - 返回的 major 保存分配到的设备号 */ static int __init mod_init(void) { major = register_chrdev(0, DEVICE_NAME, \u0026amp;fops); printk(KERN_INFO \u0026#34;simple_char: loaded, major=%d\\n\u0026#34;, major); return 0; } /* * mod_exit — 模块出口（rmmod 时执行） * __exit 标记：模块内建时不编译此函数以节省内存 * * unregister_chrdev：注销设备，释放主设备号 * 必须在 exit 中注销，否则模块卸载后 /dev/simple_char 成为空悬节点。 */ static void __exit mod_exit(void) { unregister_chrdev(major, DEVICE_NAME); printk(KERN_INFO \u0026#34;simple_char: unloaded\\n\u0026#34;); } /* 注册模块的初始化和退出函数 */ module_init(mod_init); module_exit(mod_exit); /* GPL 声明：使用 GPL 协议，不声明则内核拒绝加载或标记为污染 */ MODULE_LICENSE(\u0026#34;GPL\u0026#34;); EOF # 第 3 步：修改 Kconfig（让它在 menuconfig 中可见） # 编辑 common/drivers/misc/Kconfig，在合适位置加入： # tristate代表了三态 y/n/m m表示模块，选择y那么simple_char.c 被编译进内核，选择m的话就会生成simple_char.ko config SIMPLE_CHAR tristate \u0026#34;Simple character device driver\u0026#34; help A minimal character device driver for learning. # 第 4 步：修改 Makefile 加入编译 # 编辑 common/drivers/misc/Makefile，加入： # 这里的CONFIG_SIMPLE_CHAR依赖于第3步中配置的SIMPLE_CHAR，系统会自动变成CONFIG_SIMPLE_CHAR obj-$(CONFIG_SIMPLE_CHAR) += simple_char.o # 第 5 步：注册到 Bazel 模块输出列表 # 编辑 common/common14-5.15/common/common_drivers/modules.bzl # 在 AMLOGIC_COMMON_MODULES 列表中（按字母序）加入： # \u0026#34;drivers/misc/simple_char.ko\u0026#34;, # 否则 Bazel 虽然编译了模块但不会复制到 dist 输出目录 # modules.bzl (AMLOGIC_COMMON_MODULES 列表) # → Bazel 编译生成 simple_char.ko # → depmod 生成 modules.dep（模块依赖关系） # → amlogic_utils.sh 把 modules.dep 转成 modules.load（去掉依赖信息，只留模块名） # → 放入 out/target/product/ross/vendor_dlkm/lib/modules/modules.load # 第 6 步：编译内核模块 cd ~/android/aml/s905x5/aml-s905x5-androidu-v2/ cd common/common14-5.15/common make menuconfig #在 # 在 Device Drivers → Misc devices 中找到 Simple character device driver，设为 M（模块） make modules # 第 7 步：测试连接 # 接上板子的 adb 连接 adb devices # 预期输出：列出已连接的设备，状态为 \u0026#34;device\u0026#34; # 确认模块已加载（烧录后自动加载，无需 insmod） adb shell ls /vendor/lib/modules/simple_char.ko # 预期输出：/vendor/lib/modules/simple_char.ko（文件存在） adb shell cat /proc/devices | grep simple_char # 预期输出：类似 \u0026#34;252 simple_char\u0026#34; # 说明：主设备号（252 是示例，每次可能不同）是内核分配的，mknod 时需用这个数字 # 创建设备节点（让用户态能通过 /dev/simple_char 访问驱动） adb shell mknod /dev/simple_char c \u0026lt;上一步中的major\u0026gt; 0 adb shell chmod 666 /dev/simple_char # 说明：mknod 参数 c=字符设备，\u0026lt;major\u0026gt;=主设备号，0=次设备号 # chmod 666 让所有用户可读写，避免权限拒绝 adb shell ls -l /dev/simple_char # 预期输出：类似 \u0026#34;crw-rw-rw- 1 root root 252, 0 ... /dev/simple_char\u0026#34; # 说明：开头的 c 表示字符设备，252,0 是主次设备号 # 第 8 步：验证驱动读写 # 写入数据：用 echo 重定向到设备节点，触发驱动的 dev_write() adb shell \u0026#34;echo \u0026#39;hello from userspace\u0026#39; \u0026gt; /dev/simple_char\u0026#34; # 读出数据：用 cat 读取设备节点，触发驱动的 dev_read() adb shell cat /dev/simple_char # 预期输出：hello from userspace # 说明：读出内容应与写入的一致，证明数据经过内核缓冲区转储正确 # 查看内核日志，确认驱动各函数被成功调用 adb shell dmesg | grep simple_char # 预期输出： # simple_char: loaded, major=252 ← mod_init()，模块加载 # simple_char: open() ← dev_open()，echo 触发 open # simple_char: write() 21 bytes ← dev_write()，写入的数据长度 # simple_char: open() ← dev_open()，cat 触发 open # simple_char: read() 21 bytes ← dev_read()，读取的数据长度 验收清单 项目 验证方式 预期结果 模块已加载 cat /proc/devices | grep simple_char 输出 simple_char 及主设备号 设备节点已创建 ls -l /dev/simple_char 输出 c 开头，带主次设备号 写入再读出 echo... \u0026gt; /dev/simple_char \u0026amp;\u0026amp; cat /dev/simple_char 读出内容与写入一致 驱动函数被调用 dmesg | grep simple_char 能看到 open()/write()/read() 日志 理解 VFS 调用链 能回答\u0026quot;用户态 open 如何到达 dev_open\u0026quot; VFS → inode 设备号 → 字符设备表 → fops.open 理解 copy_to_user 能回答\u0026quot;为什么不用 memcpy\u0026quot; 安全（指针伪造）、分页（缺页）、权限（内核显式拷贝） 附：最小用户态测试程序 用 C 语言写一个测试程序，手动触发驱动的 open/write/read/close，比 shell 命令更直观地演示系统调用到驱动的完整链路：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main(void) { char wbuf[] = \u0026#34;hello driver\u0026#34;; char rbuf[64] = {0}; int fd; /* open: 用户态 open → VFS → 驱动 dev_open */ fd = open(\u0026#34;/dev/simple_char\u0026#34;, O_RDWR); if (fd \u0026lt; 0) { perror(\u0026#34;open\u0026#34;); return 1; } printf(\u0026#34;open() fd=%d\\n\u0026#34;, fd); /* write: 用户态 write → VFS → 驱动 dev_write */ write(fd, wbuf, strlen(wbuf)); printf(\u0026#34;write() %zu bytes\\n\u0026#34;, strlen(wbuf)); /* read: 用户态 read → VFS → 驱动 dev_read */ read(fd, rbuf, sizeof(rbuf)); printf(\u0026#34;read() got: %s\\n\u0026#34;, rbuf); /* close: 用户态 close → VFS → 驱动 dev_release（本例未实现，内核默认处理） */ close(fd); printf(\u0026#34;close()\\n\u0026#34;); return 0; } 编译推送到板子运行：\n# PC 端交叉编译（使用 musl 静态链接，无需依赖目标板的 libc） CLANG=~/android/aml/s905x5/aml-s905x5-androidu-v2/prebuilts/clang/host/linux-x86/clang-r487747c/bin/clang SYSROOT=~/android/aml/s905x5/aml-s905x5-androidu-v2/prebuilts/build-tools/sysroots/aarch64-unknown-linux-musl $CLANG --target=aarch64-unknown-linux-musl \\ --sysroot=$SYSROOT \\ -static \\ -rtlib=compiler-rt \\ -unwindlib=libunwind \\ -o test_simple_char test_simple_char.c # 验证二进制 file test_simple_char # 预期输出：ELF 64-bit LSB executable, ARM aarch64, statically linked ... # 推送到板子运行 adb push test_simple_char /data/local/tmp/ adb shell /data/local/tmp/test_simple_char # 预期输出： # open() fd=3 # write() 13 bytes # read() got: hello driver # close() 为什么用 musl 而不用 Android NDK 编译？ 项目的 out/soong/ndk/sysroot 只生成了 arm-linux-androideabi（32 位），没有 aarch64 的 NDK 库。但 prebuilts/build-tools/ 下有完整的 aarch64 musl 工具链（crt、libc.a、头文件齐全）。用 --target=aarch64-unknown-linux-musl + -static 编译出的二进制不依赖目标板的 libc，在任何 aarch64 Linux（包括 Android Bionic）上都能运行。本质是内核只认 syscall，不管 libc 是谁。\n对照驱动源码，每一行用户态程序都对应驱动 file_operations 中的一个函数调用。\n对照驱动源码，每一行用户态程序都对应驱动 file_operations 中的一个函数调用。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-4-%E5%AD%97%E7%AC%A6%E9%A9%B1%E5%8A%A8/","summary":"\u003ch1 id=\"level-4写一个字符设备驱动\"\u003eLevel 4：写一个字符设备驱动\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e在内核里加一个最小内核模块，用户态 \u003ccode\u003eecho \u0026quot;hi\u0026quot; \u0026gt; /dev/simple_char\u003c/code\u003e 能写进去，\u003ccode\u003ecat /dev/simple_char\u003c/code\u003e 能读出来。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e内核驱动和用户态程序的根本区别是什么？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e驱动运行在内核态，可以访问所有硬件资源和内存\u003c/li\u003e\n\u003cli\u003e用户态程序通过系统调用（open/read/write/ioctl）请求内核代为操作\u003c/li\u003e\n\u003cli\u003e内核态出错 → 整个系统崩溃（kernel panic）；用户态出错 → 只崩那个进程\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eLinux 设备驱动分哪几类？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e字符设备\u003c/strong\u003e──像文件一样 open/read/write/close，顺序访问。例子：串口（\u003ccode\u003e/dev/ttyAML0\u003c/code\u003e）、GPIO、I2C、SPI、按键。你的板子上 \u003ccode\u003els -l /dev/\u003c/code\u003e 看到前面是 \u003ccode\u003ec\u003c/code\u003e 的都是字符设备\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e块设备\u003c/strong\u003e──按块随机读写，有缓存层。例子：eMMC（\u003ccode\u003e/dev/block/mmcblk0\u003c/code\u003e）、U 盘、SD 卡。前面是 \u003ccode\u003eb\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e网络设备\u003c/strong\u003e──通过 socket 接口访问，不通过文件系统。例子：以太网（eth0）、WiFi（wlan0）。\u003ccode\u003eip link show\u003c/code\u003e 看到的就是\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e你的板子上跑 \u003ccode\u003eadb shell ls -l /dev/ | head -20\u003c/code\u003e，看看哪些是字符设备（c）、哪些是块设备（b）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e什么是 file_operations？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e它是一个结构体，里面全是函数指针\u003c/li\u003e\n\u003cli\u003e每个字符驱动需要告诉内核：\u0026ldquo;当用户调用 open() 时，执行我这个函数；调用 read() 时，执行我那个函数\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e这是一个多态机制——不同的驱动实现相同的接口，用户态不用关心底层实现\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e驱动模块的骨架\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e每个内核模块都有两个必经的函数：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003emodule_init\u003c/span\u003e(mod_init);   \u003cspan style=\"color:#75715e\"\u003e// insmod 时调用\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003emodule_exit\u003c/span\u003e(mod_exit);   \u003cspan style=\"color:#75715e\"\u003e// rmmod 时调用\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e__init\u003c/code\u003e 和 \u003ccode\u003e__exit\u003c/code\u003e 标记：这些函数在运行完后可以释放内存。\u003c/p\u003e","title":"Level 4：写一个字符设备驱动"},{"content":"Level 9：修复一个 SELinux 权限问题 我想做 当一个服务因为 SELinux 权限被拒绝而启动失败时，能独立定位并修复。这是 Android 嵌入式开发中最常见的调试场景。\n先回答三个问题 SELinux 在 Android 上到底保护什么？\n没有 SELinux：一个进程拿到 root 就可以做任何事（读所有文件、杀其他进程、控制硬件） 有 SELinux：每个进程被限制在最小的权限域中（principle of least privilege） 即使进程是 root 用户，SELinux 仍然会拒绝未经授权的操作 avc: denied = SELinux 告诉你\u0026quot;这个操作不在你的权限范围内\u0026quot; domain 和 type 是什么关系？\ndomain 是进程的标签（你是什么） type 是文件/资源的标签（这是什么） allow domain type:class { permission } = \u0026ldquo;谁 对什么 做什么\u0026rdquo; audit2allow 的原理？\n从 dmesg 或 /proc/avc 中读取 avc denied 日志 自动解析出需要的 allow 语句 但注意：audit2allow 只是工具，直接套用可能给过多权限，要人工审查 需要懂的知识 avc denied 日志解读\navc: denied { read } for pid=1234 comm=\u0026#34;luojService\u0026#34; name=\u0026#34;secret_file\u0026#34; dev=\u0026#34;mmcblk0p5\u0026#34; ino=5678 scontext=u:r:luojService:s0 tcontext=u:object_r:secret_data:s0 tclass=file permissive=0 逐字段：\ndenied { read } —— 被拒绝的操作 pid=1234 comm=\u0026quot;luojService\u0026quot; —— 哪个进程 scontext=u:r:luojService:s0 —— 进程的 domain（luojService） tcontext=u:object_r:secret_data:s0 —— 目标的类型（secret_data） tclass=file —— 目标类型分类（文件） permissive=0 —— 是否真的拒绝了（1=只警告没拦住，0=真的拦了） 转化为 allow 规则：\nallow luojService secret_data:file { read }; TE 文件的位置\nls device/amlogic/ross/sepolicy/vendor/ 动手方案 # 第 1 步：复现一个 SELinux 错误 # 用 Level 3 的 luojService，故意去掉它的 .te 文件 # 或者在 luojService 中访问一个它没权限的文件 # 第 2 步：抓取 avc 错误 adb shell dmesg | grep avc | grep luojService # 第 3 步：现场生成允许规则 adb shell dmesg | grep avc | grep luojService | audit2allow # 第 4 步：写入 .te 文件 echo \u0026#34;allow luojService some_type:file { read };\u0026#34; \u0026gt;\u0026gt; device/amlogic/ross/sepolicy/vendor/luojService.te # 第 5 步：编译烧录（参考 level-2） # 第 6 步：验证 adb shell dmesg | grep avc | grep luojService 验收清单 项目 验证方式 能制造 SELinux 错误 故意去掉权限，dmesg | grep avc 看到 denied 能修复 补上 .te 后错误消失 能解读日志 你能说出一条 avc denied 日志中每个字段的含义 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-9-selinux/","summary":"\u003ch1 id=\"level-9修复一个-selinux-权限问题\"\u003eLevel 9：修复一个 SELinux 权限问题\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e当一个服务因为 SELinux 权限被拒绝而启动失败时，能独立定位并修复。这是 Android 嵌入式开发中最常见的调试场景。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSELinux 在 Android 上到底保护什么？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e没有 SELinux：一个进程拿到 root 就可以做任何事（读所有文件、杀其他进程、控制硬件）\u003c/li\u003e\n\u003cli\u003e有 SELinux：每个进程被限制在最小的权限域中（principle of least privilege）\u003c/li\u003e\n\u003cli\u003e即使进程是 root 用户，SELinux 仍然会拒绝未经授权的操作\u003c/li\u003e\n\u003cli\u003eavc: denied = SELinux 告诉你\u0026quot;这个操作不在你的权限范围内\u0026quot;\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003edomain 和 type 是什么关系？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003edomain 是进程的标签（你是什么）\u003c/li\u003e\n\u003cli\u003etype 是文件/资源的标签（这是什么）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eallow domain type:class { permission }\u003c/code\u003e = \u0026ldquo;谁 对什么 做什么\u0026rdquo;\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eaudit2allow 的原理？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从 \u003ccode\u003edmesg\u003c/code\u003e 或 \u003ccode\u003e/proc/avc\u003c/code\u003e 中读取 avc denied 日志\u003c/li\u003e\n\u003cli\u003e自动解析出需要的 \u003ccode\u003eallow\u003c/code\u003e 语句\u003c/li\u003e\n\u003cli\u003e但注意：\u003ccode\u003eaudit2allow\u003c/code\u003e 只是工具，直接套用可能给过多权限，要人工审查\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eavc denied 日志解读\u003c/strong\u003e\u003c/p\u003e","title":"Level 9：修复一个 SELinux 权限问题"},{"content":"Level 8：App 点一下控制板子上的 LED 我想做 在手机上装一个自己写的 App，点一下按钮，板子上的物理 LED 亮或灭。打通从 App 到 GPIO 寄存器的完整链路。\n先回答三个问题 App 是怎么跟硬件通信的？\nApp (Java) ↓ Binder IPC Native Service (C++) ↓ hw_get_module() HAL (xxx_module_t) ↓ open(\u0026#34;/dev/xxx\u0026#34;) Kernel Driver ↓ writel() GPIO 寄存器 每一层都是独立的模块，层与层之间有标准接口。替换其中任何一层不影响其他层。\nHAL 解决了什么问题？\n没有 HAL 之前：App 直接 open/ioctl 设备节点 问题：驱动接口变了，所有 App 都得改 HAL 的解决：给上层提供稳定的抽象接口，驱动变化只改 HAL 实现，不涉及 Framework 和 App 在 Treble 架构中，HAL 更是 vendor 和 system 的契约边界 Binder 是什么？\nAndroid 特有的 IPC（进程间通信）机制 App 和 Native Service 是不同进程，不能直接函数调用 Binder 让它们像调用本地函数一样跨进程通信 Binder 还负责权限校验——ServiceManager 检查调用方有没有权限访问某个服务 需要懂的知识 ioctl 驱动的写法（在 Level 4 驱动基础上扩展）\n#define GPIO_LED_MAGIC \u0026#39;L\u0026#39; #define GPIO_LED_ON _IO(GPIO_LED_MAGIC, 1) #define GPIO_LED_OFF _IO(GPIO_LED_MAGIC, 2) static long gpio_led_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case GPIO_LED_ON: writel(1, gpio_base + GPIO_OUTPUT_REG); break; case GPIO_LED_OFF: writel(0, gpio_base + GPIO_OUTPUT_REG); break; } return 0; } 在 .file_operations 中加入 .unlocked_ioctl = gpio_led_ioctl\nHAL 模块模板\nHAL 本质上是一个约定格式的动态库（.so），它必须暴露一个名为 HAL_MODULE_INFO_SYM 的符号：\nconst struct hw_module_t HAL_MODULE_INFO_SYM = { .tag = HARDWARE_MODULE_TAG, .id = \u0026#34;led\u0026#34;, .methods = \u0026amp;led_module_methods, }; 上层通过 hw_get_module(\u0026quot;led\u0026quot;, \u0026amp;module) 加载这个 so，然后通过 module-\u0026gt;methods-\u0026gt;open() 拿到设备操作接口。\nAIDL 与 Binder Service\nService 端实现一个接口（如 ILedService），注册到 ServiceManager：\ndefaultServiceManager()-\u0026gt;addService(\u0026#34;led\u0026#34;, new LedService()); // App 端获取 sp\u0026lt;IBinder\u0026gt; binder = defaultServiceManager()-\u0026gt;getService(\u0026#34;led\u0026#34;); sp\u0026lt;ILedService\u0026gt; service = ILedService::asInterface(binder); service-\u0026gt;setLedOn(); 动手方案 （各层代码从 Level 3、4、5 复用和扩展）\n# 第 1 步：确认驱动已有 ioctl（扩展 Level 4 的驱动） # 在 simple_char.c 中加入 GPIO_LED_ON/OFF 的 ioctl 命令 # 第 2 步：写 HAL 模块 # 位置：hardware/amlogic/led/ 或 vendor/amlogic/hardware/led/ # 第 3 步：写 Native Service # 位置：device/amlogic/ross/ledservice/ # 第 4 步：写 App # 用 Android Studio 或 aapt 编译 # 第 5 步：编译烧录（参考 level-2） 验收清单 项目 验证方式 全链路通 App 点按钮，板子上 LED 亮/灭 理解每层作用 你能画出去掉每一层后系统会怎样（例如没有 HAL 行不行） SELinux 没有拦 dmesg | grep avc 没有你的服务的错误 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-8-%E5%85%A8%E9%93%BE%E8%B7%AF/","summary":"\u003ch1 id=\"level-8app-点一下控制板子上的-led\"\u003eLevel 8：App 点一下控制板子上的 LED\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e在手机上装一个自己写的 App，点一下按钮，板子上的物理 LED 亮或灭。打通从 App 到 GPIO 寄存器的完整链路。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eApp 是怎么跟硬件通信的？\u003c/strong\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eApp (Java)\n  ↓ Binder IPC\nNative Service (C++)\n  ↓ hw_get_module()\nHAL (xxx_module_t)\n  ↓ open(\u0026#34;/dev/xxx\u0026#34;)\nKernel Driver\n  ↓ writel()\nGPIO 寄存器\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e每一层都是独立的模块，层与层之间有标准接口。替换其中任何一层不影响其他层。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHAL 解决了什么问题？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e没有 HAL 之前：App 直接 open/ioctl 设备节点\u003c/li\u003e\n\u003cli\u003e问题：驱动接口变了，所有 App 都得改\u003c/li\u003e\n\u003cli\u003eHAL 的解决：给上层提供稳定的抽象接口，驱动变化只改 HAL 实现，不涉及 Framework 和 App\u003c/li\u003e\n\u003cli\u003e在 Treble 架构中，HAL 更是 vendor 和 system 的契约边界\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eBinder 是什么？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAndroid 特有的 IPC（进程间通信）机制\u003c/li\u003e\n\u003cli\u003eApp 和 Native Service 是不同进程，不能直接函数调用\u003c/li\u003e\n\u003cli\u003eBinder 让它们像调用本地函数一样跨进程通信\u003c/li\u003e\n\u003cli\u003eBinder 还负责权限校验——ServiceManager 检查调用方有没有权限访问某个服务\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eioctl 驱动的写法（在 Level 4 驱动基础上扩展）\u003c/strong\u003e\u003c/p\u003e","title":"Level 8：App 点一下控制板子上的 LED"},{"content":"Level 7：追踪完整启动流程 我想做 从上电到 Launcher，完整追踪一次 S905X5M 的启动过程，每个阶段都能在代码中找到对应位置。\n先回答三个问题 Amlogic 的启动链和通用 Linux 有什么不同？\n通用嵌入式：ROM Code → Bootloader → Kernel → init Amlogic：ROM Code → BL2（DDR 初始化） → BL30/SPL → U-Boot → Kernel → init 多出来的 BL2 和 BL30 是 Amlogic 私有的二进制固件 ROM Code 怎么找到下一阶段的代码？\n芯片上电后，内部 ROM 中的固化代码执行 根据 boot mode 引脚的电平（eMMC/SD 卡/USB burning），去对应介质读取 bootloader 这是芯片设计时固定的，不能修改 内核解压后，第一个用户态进程是什么？\nPID 1 → init 进程 init 解析 init.rc → 启动核心服务（servicemanager、surfaceflinger 等） zygote（Android Java 虚拟机） → 启动 system_server → 启动 Launcher 需要懂的知识 启动级联图\n时间轴 → ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌────────┐ ┌──────────┐ │ ROM Code │ → │ BL2 │ → │ BL30/SPL │ → │ U-Boot │ → │ Kernel │ → │ init │ │ (芯片内) │ │(DDR初始化)│ │(安全启动) │ │(加载内核)│ │(驱动初始化)│ │(用户态入口)│ └──────────┘ └─────────┘ └──────────┘ └──────────┘ └────────┘ └──────────┘ 串口: GX2A:BL: BL2/BL30 U-Boot Booting init: \u0026#34;GX2A:BL:\u0026#34; \u0026#34;DDR init\u0026#34; 加载完成 版本信息 Linux... starting... 完整链路持续到 Launcher：\ninit → zygote → system_server → ActivityManager → Launcher 串口日志中每一阶段的标志\n阶段 串口中找这个 说明 ROM Code GX2A:BL: 或 G12A:BL: 芯片型号前缀 DDR 初始化 DDR init complete BL2 完成内存训练 U-Boot U-Boot 20xx.xx U-Boot 版本和编译时间 Kernel Booting Linux on physical CPU 内核开始启动 init init: init first stage started init 进程启动 Zygote Zygote: Zygote main Java 虚拟机启动 System Server SystemServer: Entered the Android system server 系统服务就绪 Launcher Displayed org.xxx/.Launcher 桌面显示完成 动手方案 # 第 1 步：接串口，抓一份完整启动日志 # 重新上电，从开机到桌面完全显示 # 保存日志：screen 中用 Ctrl+A → H 记录日志 # 第 2 步：标注日志中的关键事件 grep -n \u0026#34;GX2A:BL\\|DDR init\\|U-Boot\\|Booting Linux\\|init first stage\\|Zygote:\\|SystemServer:\\|Displayed\u0026#34; boot.log # 第 3 步：在代码中找到对应的源文件 # U-Boot 源码 find ~/android/aml/s905x5/aml-s905x5-androidu-v2/ -name \u0026#34;u-boot\u0026#34; -type d 2\u0026gt;/dev/null # 设备树 ls ~/android/aml/s905x5/aml-s905x5-androidu-v2/common/arch/arm64/boot/dts/amlogic/*ross* # init.rc 核心文件 adb shell cat /init.rc | head -30 # Amlogic 特有的服务配置 find ~/android/aml/s905x5/aml-s905x5-androidu-v2/device/amlogic/ross/ -name \u0026#34;*.rc\u0026#34; # 第 4 步：找 Amlogic 特有进程 adb shell ps -A | grep -E \u0026#34;ge2d|mediahal|droidvold|aml\u0026#34; # 第 5 步：绘制自己的启动时序图 # +0.000s ROM Code: GX2A:BL:... # +0.050s BL2: DDR init complete # +0.200s U-Boot: U-Boot 2023.01 # +1.500s Kernel: Booting Linux on physical CPU # +3.000s init: init first stage started # +10.00s Zygote: Zygote main # +18.00s SystemServer: Entered the Android system server # +25.00s Launcher: Displayed ... 验收清单 项目 验证方式 完整启动日志 记录从 ROM Code 到 Launcher 的串口输出 找到 u-boot 源码路径 笔记中记录了实际路径 标注关键事件 能在日志中圈出至少 5 个阶段的事件点 手绘时序图 画出一张 A4 纸的启动级联图，每个阶段注明对应代码位置 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-7-%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/","summary":"\u003ch1 id=\"level-7追踪完整启动流程\"\u003eLevel 7：追踪完整启动流程\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e从上电到 Launcher，完整追踪一次 S905X5M 的启动过程，每个阶段都能在代码中找到对应位置。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAmlogic 的启动链和通用 Linux 有什么不同？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e通用嵌入式：ROM Code → Bootloader → Kernel → init\u003c/li\u003e\n\u003cli\u003eAmlogic：ROM Code → BL2（DDR 初始化） → BL30/SPL → U-Boot → Kernel → init\u003c/li\u003e\n\u003cli\u003e多出来的 BL2 和 BL30 是 Amlogic 私有的二进制固件\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eROM Code 怎么找到下一阶段的代码？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e芯片上电后，内部 ROM 中的固化代码执行\u003c/li\u003e\n\u003cli\u003e根据 boot mode 引脚的电平（eMMC/SD 卡/USB burning），去对应介质读取 bootloader\u003c/li\u003e\n\u003cli\u003e这是芯片设计时固定的，不能修改\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e内核解压后，第一个用户态进程是什么？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePID 1 → init 进程\u003c/li\u003e\n\u003cli\u003einit 解析 \u003ccode\u003einit.rc\u003c/code\u003e → 启动核心服务（servicemanager、surfaceflinger 等）\u003c/li\u003e\n\u003cli\u003ezygote（Android Java 虚拟机） → 启动 system_server → 启动 Launcher\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"需要懂的知识\"\u003e需要懂的知识\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e启动级联图\u003c/strong\u003e\u003c/p\u003e","title":"Level 7：追踪完整启动流程"},{"content":"Level 6：GPIO 按键 — 从设备树到 input 子系统 我想做 板子上有一个丝印 USER_KEY 的物理按键（接在 GPIOD_2），按下它能被 Linux input 子系统识别到事件，并理解从 DTS → 驱动 → sysfs → /dev/input/eventX 的完整链路。\n先回答三个问题 Linux 怎么知道一个 GPIO 是按键而不是 LED？\nDTS 中的 compatible 决定匹配哪个驱动。LED 用 \u0026quot;gpio-leds\u0026quot;，按键用 \u0026quot;amlogic, gpio_keypad\u0026quot;（或主线 \u0026quot;gpio-keys\u0026quot;） 驱动根据 compatible 字符串被内核匹配并 probe，在 probe 中注册 input 设备 按键事件是怎么从硬件传到 App 的？\n手指按下 → GPIO 电平变化 → 驱动检测（polling/irq） → input_report_key() → input_sync() → /dev/input/eventX → App 通过 getevent / InputReader 读取 每一层都是标准接口，App 不关心 GPIO 编号，只读 keycode。\nAmlogic 的 gpio_keypad 和主线 gpio-keys 有什么区别？\n主线 gpio-keys：通用驱动，每个按键独立描述为子节点 Amlogic gpio_keypad：私有驱动，用一个节点描述多个按键（key_num + 数组式属性），额外提供 /sys/class/gpio_keypad/ 接口和蓝牙键计数功能 需要懂的知识 input 子系统\nLinux 内核用 input 子系统统一管理所有输入设备（键盘、鼠标、触摸屏、遥控器等）。核心概念：\ninput_dev：一个输入设备，驱动通过 input_register_device() 注册 input_event()：报告一个输入事件（类型 + 代码 + 值） input_sync()：标记一帧事件结束，用户态据此判断读取可以停止 /dev/input/eventX：用户态读取事件的字符设备 关键类型：\nEV_KEY // 按键（KEY_VOLUMEUP, KEY_POWER, KEY_HOME ...） EV_SW // 开关（SW_MUTE_DEVICE ...） EV_ABS // 绝对坐标（触摸屏） EV_REL // 相对坐标（鼠标） DTS 中如何描述一个 GPIO 按键\nAmlogic gpio_keypad 节点的每一行含义（以 S7D bm201 为例）：\ngpio_keypad { compatible = \u0026#34;amlogic, gpio_keypad\u0026#34;; // 匹配 Amlogic 私有驱动 status = \u0026#34;okay\u0026#34;; scan_period = \u0026lt;20\u0026gt;; // 轮询周期 20ms key_num = \u0026lt;2\u0026gt;; // 共 2 个按键 key_name = \u0026#34;bluetooth\u0026#34;, \u0026#34;mute\u0026#34;; // 按键名称（数组） key_code = \u0026lt;600 SW_MUTE_DEVICE\u0026gt;; // Linux keycode（数组） key_type = \u0026lt;EV_KEY EV_SW\u0026gt;; // 事件类型（数组） key-gpios = \u0026lt;\u0026amp;gpio GPIOD_2 GPIO_ACTIVE_HIGH // GPIO 引脚（数组） \u0026amp;gpio GPIOD_3 GPIO_ACTIVE_HIGH\u0026gt;; /* 0:polling mode, 1:irq mode */ detect_mode = \u0026lt;0\u0026gt;; // 检测模式 }; 注意：DTS 中 key_name = \u0026quot;bluetooth\u0026quot; 就是板子丝印上的 USER_KEY。key_code = 600 是厂商自定义 code（非标准 Linux keycode）。\nGPIO 编号规则（S7D）\n来自 meson-s7d-gpio.h——不同 SoC 编号完全不同，必须查对应当前芯片的头文件：\nGPIOD_0 = 24, GPIOD_1 = 25, GPIOD_2 = 26, GPIOD_3 = 27, GPIOD_4 = 28 S7D pin 排列顺序：E → B → C → D → DV → H → X → Z → TEST_N → CC（共 83 pins）。\npolling 和 interrupt 两种检测模式\n模式 detect_mode 原理 优缺点 polling 0 定时器每 20ms 读一次 GPIO 电平 CPU 持续消耗，但实现简单，不会丢事件 irq 1 注册 GPIO 中断，边沿触发后启动 timer 消抖 CPU 开销低，但中断风暴时可能丢事件 板子默认使用 polling 模式。\n驱动中的消抖（debounce）机制\n机械按键按下/释放时电平会抖动：\n理想波形： ──┐ ┌── 实际波形： ──┐┌┐┌┐┌┐┌┐┌┐┌┐┌── 驱动通过 KEY_JITTER_COUNT = 1 消抖：连续 2 次读到相同值才确认状态变化（gpio_keypad.c:64）。\n蓝牙键的特殊计数逻辑\n驱动中硬编码了对 \u0026quot;bluetooth\u0026quot; 按键的计数（gpio_keypad.c:76-78）：\nif (strcmp(key-\u0026gt;name, \u0026#34;bluetooth\u0026#34;) == 0) { keypad-\u0026gt;reset_count++; } 这个计数暴露在 /sys/class/resetkey/count，通常用于实现\u0026quot;长按蓝牙键 N 次恢复出厂设置\u0026quot;。\n动手方案 第 1 步：在板子上确认按键对应的 input 设备 # 列出所有 input 设备 adb shell getevent -l # 在输出中找 gpio_keypad： # /dev/input/event3: gpio_keypad # 记下这个 event 编号（假设是 event3） 第 2 步：监听按键事件 # 实时监听按键事件，按下板子上的 USER_KEY 观察输出 adb shell getevent -l /dev/input/event3 # 按下时输出类似： # EV_KEY 00000258 DOWN ← 0258 = 600 = bluetooth keycode # EV_SYN SYN_REPORT 00000000 # 释放时输出类似： # EV_KEY 00000258 UP # EV_SYN SYN_REPORT 00000000 第 3 步：查看 sysfs 接口 # 查看所有按键的当前状态 adb shell cat /sys/class/gpio_keypad/table # 输出： # [0]: name = bluetooth status = 1 ← 1=释放, 0=按下 # [1]: name = mute status = 1 # 查看蓝牙键按下次数 adb shell cat /sys/class/resetkey/count # 按几下 USER_KEY 再读，数字会递增 第 4 步：控制是否忽略指定按键 # 忽略蓝牙键（按下后无事件） adb shell \u0026#34;echo \u0026#39;bluetooth,600 Y\u0026#39; \u0026gt; /sys/class/gpio_keypad/ignore\u0026#34; # 此时按 USER_KEY，getevent 不会再收到事件 # 恢复 adb shell \u0026#34;echo \u0026#39;bluetooth,600 N\u0026#39; \u0026gt; /sys/class/gpio_keypad/ignore\u0026#34; # 忽略所有按键 adb shell \u0026#34;echo \u0026#39;all 1\u0026#39; \u0026gt; /sys/class/gpio_keypad/ignore\u0026#34; # 恢复所有按键 adb shell \u0026#34;echo \u0026#39;none 1\u0026#39; \u0026gt; /sys/class/gpio_keypad/ignore\u0026#34; 第 5 步：从内核日志确认驱动加载 # 查看 gpio_keypad 驱动的 probe 日志 adb shell dmesg | grep -i \u0026#34;gpio.keypad\\|gpio_keypad\u0026#34; # 查看 GPIOD_2 的 GPIO 占用 adb shell cat /sys/kernel/debug/gpio | grep -i \u0026#34;GPIOD_2\\|bluetooth\u0026#34; 第 6 步：追踪驱动源码调用链 从 DTS probe 到按键事件上报的完整调用链：\nplatform_driver_register(\u0026amp;meson_gpio_kp_driver) → meson_gpio_kp_probe() // gpio_keypad.c:264 → devm_gpiod_get_index() // 获取 GPIOD_2、GPIOD_3 的 GPIO 描述符 → of_property_read_string_index() // 解析 key_name[] → gpiod_direction_input() // 设为输入模式 → gpiod_set_pull(PULL_UP) // 使能内部上拉 → input_register_device() // 注册 input 设备 → mod_timer(\u0026amp;polling_timer) // 启动 20ms 定时轮询 定时器触发: → polling_timer_handler() // gpio_keypad.c:89 → gpiod_get_value() // 读 GPIO 电平 → report_key_code() // gpio_keypad.c:60 → input_event(EV_KEY, code, val)// 报告按键事件 → input_sync() // 同步帧 关键文件路径速查 设备树（DTS） 用途 路径 板级 DTS（gpio_keypad 节点） common/common_drivers/arch/arm64/boot/dts/amlogic/s7d_s905x5m_bm201.dts:781 GPIO pin 编号宏（GPIOD_2 = 26） common/common_drivers/include/dt-bindings/gpio/meson-s7d-gpio.h:42 按键驱动 用途 路径 Amlogic gpio_keypad 驱动 common/common_drivers/drivers/input/keyboard/gpio_keypad.c 驱动 Makefile common/common_drivers/drivers/input/keyboard/Makefile 主线 gpio-keys 驱动（对照参考） common/drivers/input/keyboard/gpio_keys.c 主线 gpio-keys 驱动（polled 版本） common/drivers/input/keyboard/gpio_keys_polled.c GPIO / pinctrl 驱动 用途 路径 S7D pinctrl 驱动 common/common_drivers/drivers/gpio/pinctrl/pinctrl-meson-s7d.c Meson pinctrl 通用驱动 common/drivers/pinctrl/meson/pinctrl-meson.c gpiolib 核心 common/drivers/gpio/gpiolib.c gpiolib OF 解析 common/drivers/gpio/gpiolib-of.c 板子上用户态路径 用途 命令/路径 监听按键事件 adb shell getevent -l /dev/input/eventX 查看按键状态表 cat /sys/class/gpio_keypad/table 查看/控制按键忽略 cat /sys/class/gpio_keypad/ignore 查看蓝牙键计数 cat /sys/class/resetkey/count 查看 GPIO 占用 cat /sys/kernel/debug/gpio 延伸阅读 驱动代码的逐段深度分析（数据结构设计、probe 流程、input 子系统集成、sysfs class 接口、电源管理等）已整理至 level-6-总结与扩展.md。\n验收清单 项目 验证方式 找到按键对应的 input 设备 getevent -l 看到 gpio_keypad 按下按键看到事件 getevent -l /dev/input/eventX 看到 KEY DOWN/UP 理解 sysfs 接口 能查看 table，会用 ignore 开关按键 理解 DTS 节点 能说清 DTS 中 key_name、key_code、key-gpios 的对应关系 理解消抖机制 能解释为什么需要 KEY_JITTER_COUNT 区分 polling vs irq 能从 DTS detect_mode 判断当前模式，并说清两种模式的优缺点 ","permalink":"https://luojmarkdown.pages.dev/posts/%E7%BB%83%E4%B9%A0%E5%AE%9E%E6%93%8D/level-6-%E6%8C%89%E9%94%AE/","summary":"\u003ch1 id=\"level-6gpio-按键--从设备树到-input-子系统\"\u003eLevel 6：GPIO 按键 — 从设备树到 input 子系统\u003c/h1\u003e\n\u003ch2 id=\"我想做\"\u003e我想做\u003c/h2\u003e\n\u003cp\u003e板子上有一个丝印 \u003cstrong\u003eUSER_KEY\u003c/strong\u003e 的物理按键（接在 \u003cstrong\u003eGPIOD_2\u003c/strong\u003e），按下它能被 Linux input 子系统识别到事件，并理解从 DTS → 驱动 → sysfs → /dev/input/eventX 的完整链路。\u003c/p\u003e\n\u003ch2 id=\"先回答三个问题\"\u003e先回答三个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eLinux 怎么知道一个 GPIO 是按键而不是 LED？\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDTS 中的 \u003ccode\u003ecompatible\u003c/code\u003e 决定匹配哪个驱动。LED 用 \u003ccode\u003e\u0026quot;gpio-leds\u0026quot;\u003c/code\u003e，按键用 \u003ccode\u003e\u0026quot;amlogic, gpio_keypad\u0026quot;\u003c/code\u003e（或主线 \u003ccode\u003e\u0026quot;gpio-keys\u0026quot;\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e驱动根据 compatible 字符串被内核匹配并 probe，在 probe 中注册 input 设备\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e按键事件是怎么从硬件传到 App 的？\u003c/strong\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e手指按下 → GPIO 电平变化 → 驱动检测（polling/irq）\n  → input_report_key() → input_sync()\n    → /dev/input/eventX → App 通过 getevent / InputReader 读取\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e每一层都是标准接口，App 不关心 GPIO 编号，只读 keycode。\u003c/p\u003e","title":"Level 6：GPIO 按键 — 从设备树到 input 子系统"},{"content":"1. 目录结构扫描与功能说明 一级目录 二级目录 (关键) 作用说明 APP 相关标注 amlogic/ common/apps/ Amlogic 通用第三方或预置 APP (如 Netflix) APP 源码/APK 存放地 reference/apps/ Amlogic 参考应用 (如 TVInput, DLNA) APP 源码存放地 common/sepolicy/ Amlogic 通用 SELinux 策略 权限配置 giec/ apps/ GIEC 厂商自定义 APP (如 Glauncher, OTAClient) 核心 APP 存放地 common/sepolicy/ GIEC 厂商自定义 SELinux 策略 权限配置 android-certs/ 厂商签名密钥 (platform, shared 等) 签名资源 executable/ 厂商自定义脚本与二进制工具 运行辅助 widevine/ libwvdrmengine/ Widevine DRM 相关组件 辅助组件 2. 构建文件 (Android.mk/Android.bp) 分析 (1) 预置 APK 模式 (以 Glauncher 为例) 在 Glauncher/Android.mk 中：\nLOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := Glauncher LOCAL_SRC_FILES := Glauncher.apk LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform # 使用系统签名 LOCAL_PRIVILEGED_MODULE := true # 声明为特权应用 (安装到 /system/priv-app) include $(BUILD_PREBUILT) # 使用预编译规则 (2) 源码编译模式 (以 ResolutionModeTester 为例) 在 ResolutionModeTester/Android.bp 中：\nandroid_app { name: \u0026#34;ResolutionModeTester\u0026#34;, srcs: [\u0026#34;src/**/*.java\u0026#34;], certificate: \u0026#34;platform\u0026#34;, # 签名类型 privileged: true, # 特权应用 platform_apis: true, # 使用系统 API static_libs: [\u0026#34;SettingsLib\u0026#34;], # 依赖库 } 3. 添加 APP 脚本与工具流程 device-giec.mk 承担了“注册入口”的角色。其添加流程如下：\ngraph TD A[准备 APK/源码] --\u0026gt; B[创建子目录 \u0026amp; 编写 Android.mk/bp] B --\u0026gt; C[在 device-giec.mk 中添加 PRODUCT_PACKAGES] C --\u0026gt; D{是否需要系统权限?} D -- 是 --\u0026gt; E[在 sepolicy/ 中添加 .te 规则] D -- 是 --\u0026gt; F[在 privapp-permissions.xml 中添加白名单] D -- 否 --\u0026gt; G[开始编译 make -jN] E --\u0026gt; G F --\u0026gt; G G --\u0026gt; H[验证 APK 是否在 /vendor/app 或 /system/priv-app] 4. 初始化与系统服务 (init.rc) giec.hardware.hwstbcmdservice@1.0-service.rc 展示了如何为 APP 相关的后台服务配置启动项：\nservice vendor.hwstbcmdservice-1-0 /vendor/bin/hw/giec.hardware.hwstbcmdservice@1.0-service class hal user system group system 5. SELinux 权限配置 (sepolicy/) 在 system_app.te 中，为厂商应用授予访问硬件服务的权限：\n# 允许 system_app 查找和调用厂商自定义的 HIDL 服务 allow system_app vnd_hwstbcmdservice_hwservice:hwservice_manager { find }; allow system_app hal_hwstbcmdservice:binder { call transfer }; 6. 预置 APK 统计与白名单 预置 APK 数量：约 5-8 个（包括 Glauncher, LeanKeyboard, OTAClient, STB-TEST 等）。 权限白名单：参考 droidlogic_permissions.xml，若新增 APP 为 priv-app 且需 SET_TIME 等敏感权限，必须在此类 XML 中声明。 7. 《vendor/giec APP 添加规范》 (1) 最小改动模板 (Android.mk) LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := MyNewApp LOCAL_SRC_FILES := MyNewApp.apk LOCAL_MODULE_CLASS := APPS LOCAL_CERTIFICATE := platform LOCAL_PRIVILEGED_MODULE := true include $(BUILD_PREBUILT) (2) 注册入口 (device-giec.mk) # 在 vendor/giec/device-giec.mk 中添加： PRODUCT_PACKAGES += MyNewApp (3) 验证与回退 验证步骤： make MyNewApp：单独编译模块确认路径无误。 make -j32：整机编译。 adb shell pm list packages | grep mynewapp：检查是否安装成功。 adb logcat | grep -i MyNewApp：检查启动日志。 回退方案： 从 PRODUCT_PACKAGES 中删除模块名。 删除 vendor/giec/apps/MyNewApp 目录。 执行 make clean 或删除 out/ 下对应产物后重新编译。 总结建议 新增 APP 应优先放置在 vendor/giec/apps/ 下，通过修改 vendor/giec/device-giec.mk 进行集成。若涉及系统 API 调用，务必在 sepolicy/ 下同步更新策略，否则会导致应用闪退或功能失效。\n实战总结 TiviMateCustom.apk（本身就是从应用市场下载的所以签名需要为PRESIGNED）\n需求：\n安装到/product/app下（LOCAL_PRODUCT_MODULE := true） 无需特权（LOCAL_PRIVILEGED_MODULE := false） 签名为PRESIGNED（LOCAL_CERTIFICATE := PRESIGNED） 遇到问题 1.共享库声明问题 APK 自身的 AndroidManifest.xml 中会声明 必须/可选共享库，与此对应的构建系统的 Android.mk 也需要声明对应的 必须/可选共享库，否则编译会报错\nmanifest 中的声明 ：uses-library:\u0026lsquo;xxx\u0026rsquo;（必需），uses-library-not-required:\u0026lsquo;xxx\u0026rsquo;（可选）\nAndroid.mk 中对应：LOCAL_REQUIRED_USES_LIBRARIES，LOCAL_OPTIONAL_USES_LIBRARIES\n对于第三方APK如何查询其的manifest\n1. aapt dump badging — 最常用，直接列出 uses-library aapt dump badging xxx.apk | grep uses-library 2. aapt dump xmltree — 看完整 manifest aapt dump xmltree xxx.apk AndroidManifest.xml ● AndroidManifest.xml 是每个 APK 的身份说明书，编译时由构建工具生成、打包进 APK。它告诉 Android 系统：我是谁、我能干什么、我需要什么。\n主要声明的内容：\n包名 │ package=\u0026ldquo;com.example.app\u0026rdquo; │ 唯一标识这个 App\n四大组件 │ activity, service, receiver, provider\t│ 注册入口点，系统才知道怎么启动\n权限 │ uses-permission │ 请求系统权限\n系统特性 │ uses-feature │ 声明依赖的硬件/软件能力（如摄像头）\n共享库依赖 │ uses-library │ 声明依赖的系统共享库\nSDK 版本 │ minSdkVersion, targetSdkVersion │ 兼容范围\n2.非法字节码问题 重打包工具在修改 APK 时（常见于破解/修改版APK）会粗劣注入非法字节码，导致编译不过。\n此时需要加上\nLOCAL_DEX_PREOPT := false dexpreopt = DEX Pre-Optimization，即预优化。调用 dex2oat 将 APK 中的 .dex 字节码提前编译为目标设备的机器码（.odex 文件），并在此过程中做完整的字节码验证。\n预置 TiviMateCustom 技术报告 一、APK 初始状态验证 1.1 签名验证 # 验证签名方案和证书 out/host/linux-x86/bin/apksigner verify --verbose --print-certs zip/TiviMateCustom.apk 结果：\nVerified using v1 scheme (JAR signing): true Verified using v2 scheme (APK Signature Scheme v2): true Verified using v3 scheme (APK Signature Scheme v3): true Signer #1 certificate DN: CN=PI Connect, OU=PURE, O=PIConnect, L=Paris, ST=IDF, C=FR 签名方案 v1+v2+v3 均有效，签名者 PI Connect（非 TiviMate 官方开发者 AR Mobile）。\n1.2 压缩方式验证 构建系统要求 .so 和 .dex 在 APK 中必须为 store（不压缩） 以便 mmap 直接映射。检查方法：\n# 检查 .so 和 .dex 的压缩状态 # defN = 压缩, stor = 不压缩 zipinfo zip/TiviMateCustom.apk \u0026#39;lib/*/*.so\u0026#39; \u0026#39;*.dex\u0026#39; 2\u0026gt;/dev/null | grep -v \u0026#39; stor \u0026#39; 结果：55 个文件为压缩状态（3 个 .dex + 52 个 .so 全部为 defN），不满足系统预置要求。\n1.3 对齐验证 # -c 只检查不修改, -p 4 要求 4 字节对齐 out/host/linux-x86/bin/zipalign -c -p 4 zip/TiviMateCustom.apk 结果：通过，APK 已满足 4 字节对齐。\n1.4 破解来源验证 方法一：查 manifest 中的 uses-library\naapt dump badging zip/TiviMateCustom.apk | grep -E \u0026#34;(package:|launchable-activity:)\u0026#34; 输出：\npackage: name=\u0026#39;ar.tvplayer.tv\u0026#39; versionCode=\u0026#39;5190\u0026#39; versionName=\u0026#39;5.1.9\u0026#39; compileSdkVersion=\u0026#39;35\u0026#39; launchable-activity: name=\u0026#39;com.andyhax.haxsplash.LaunchActivity\u0026#39; 包名 ar.tvplayer.tv 是 TiviMate 官方包名，但启动 Activity 被替换为 com.andyhax.haxsplash.LaunchActivity（AndyHax 注入劫持的入口）。\n方法二：查 classes.dex 中的注入框架类名\nunzip -p zip/TiviMateCustom.apk classes.dex | strings | grep -iE \u0026#34;killer|hack|signaturertx|rebrand|hook|xposed\u0026#34; | sort -u 关键发现：\n类名 来源 功能 bin.mt.signaturertx.KillerApplication MT 管理器 签名校验杀手，接受任何签名 com.andyhax.haxsplash.LaunchActivity AndyHax 劫持启动页 com.andyhax.hook.HookApplication AndyHax 运行时 Hook 注入 rtx.app.RTXRebrand RTX Rebrand 换皮/重命名 org.lsposed.hiddenapibypass.HiddenApiBypass LSPosed 隐藏 API 绕过 com.bytedance.shadowhook.ShadowHook ByteDance PLT Hook 库 com.bytedance.bytehook.ByteHook ByteDance 字节码/方法 Hook 方法三：查签名字段\nout/host/linux-x86/bin/apksigner verify --verbose --print-certs zip/TiviMateCustom.apk | grep \u0026#34;certificate DN\u0026#34; 输出：CN=PI Connect, OU=PURE, O=PIConnect —— 非 TiviMate 官方（AR Mobile），且是自签名证书。\n结论：此 APK 是破解版，包含至少 3 层注入（签名杀手 + Hook 框架 + 换皮工具）、DEX 保护壳、字节码混淆。\n二、do_not_alter_apk 分叉逻辑 文件: build/make/core/app_prebuilt_internal.mk\nbuild/make/core/app_prebuilt_internal.mk L194 do_not_alter_apk := L195 ifeq (PRESIGNED,$(LOCAL_CERTIFICATE)) L196 ifneq (,$(LOCAL_SDK_VERSION)) L197 ifeq ($(call math_is_number,$(LOCAL_SDK_VERSION)),true) L198 ifeq ($(call math_gt,$(LOCAL_SDK_VERSION),29),true) L199 do_not_alter_apk := true L200 endif L201 endif L202 # TODO: Add system_current after fixing the existing modules. L203 ifneq ($(filter current test_current core_current,$(LOCAL_SDK_VERSION)),) L204 do_not_alter_apk := true L205 endif L206 endif L207 endif 触发 do_not_alter_apk := true 的条件：PRESIGNED 且 LOCAL_SDK_VERSION 为数字且 \u0026gt; 29。\n路径 1：do_not_alter_apk = true（纯拷贝 + 检查） build/make/core/app_prebuilt_internal.mk L209 ifeq ($(do_not_alter_apk),true) L210 $(built_module) : $(my_prebuilt_src_file) | $(ZIPALIGN) L211 $(transform-prebuilt-to-target) # 纯拷贝 L212 $(check-jni-dex-compression) # 检查 .so/.dex 是否 store L213 $(check-package-alignment) # 检查是否 4 字节对齐 check-jni-dex-compression 定义：\nbuild/make/core/definitions.mk L3011 define check-jni-dex-compression L3012 if (zipinfo $@ \u0026#39;lib/*.so\u0026#39; \u0026#39;*.dex\u0026#39; 2\u0026gt;/dev/null | grep -v \u0026#39; stor \u0026#39; \u0026gt;/dev/null) ; then \\ L3013 $(call echo-error,$@,Contains compressed JNI libraries and/or dex files); \\ L3014 exit 1; \\ L3015 fi L3016 endef 此检查硬编码，无变量可以跳过。\n路径 2：do_not_alter_apk = false（拆包修改） build/make/core/app_prebuilt_internal.mk L241 $(built_module) : $(my_prebuilt_src_file) | $(ZIPALIGN) $(ZIP2ZIP) $(SIGNAPK_JAR) $(SIGNAPK_JNI_LIBRARY_PATH) L242 $(transform-prebuilt-to-target) # 拷贝 L243 $(uncompress-prebuilt-embedded-jni-libs) # 解压 .so 文件 L244 $(remove-unwanted-prebuilt-embedded-jni-libs) # 删除多余架构 .so L248 ifneq ($(LOCAL_CERTIFICATE),PRESIGNED) L253 $(sign-package) # 重签名 L255 else # LOCAL_CERTIFICATE == PRESIGNED L256 $(align-package) # zipalign 重打包 L257 endif 为什么路径 2 会破坏签名 v1、v2、v3 三种签名都不容忍 APK 内容变化：\nv1 (JAR 签名)：META-INF/MANIFEST.MF 逐文件记录 SHA1 哈希。ZIP 条目压缩方式变化 → CRC-32 变化 → 哈希不匹配 v2/v3 (APK Signature Scheme)：对整个 APK 文件二进制做签名。ZIP 内容区任何字节变化 → APK Signing Block 校验不匹配 验证方法：对比修改前后的签名：\n# 原始 APK out/host/linux-x86/bin/apksigner verify --verbose zip/TiviMateCustom.apk # 设备上拉下来的（路径 2 产出的） out/host/linux-x86/bin/apksigner verify --verbose zip/test.apk 原始 APK test.apk (路径 2 产物) v1 true false v2 true false v3 true false 三、踩坑链路 # 错误信息 环节 原因 根本原因 1 mismatch in \u0026lt;uses-library\u0026gt; tags manifest_check.py 构建验证 APK manifest 声明了 3 个可选共享库，Android.mk 未声明 破解包使用了非标准库 2 constructors can't be abstract or native dex2oat verify 验证 uses-library 通过 → filter 升为 verify → 暴露破解工具注入的非法字节码 破解工具 patch 的字节码不符合 JVM 规范 3 APK 从 59MB 变为 74MB + INSTALL_PARSE_FAILED_NO_CERTIFICATES 路径 2 拆包重压 uncompress-jni + align-package 改变压缩方式（52 个文件 deflate → store），PRESIGNED 跳过重签 破解包 v2/v3 被砍掉，仅剩的 v1 也因重压失效 4 check-jni-dex-compression FAIL 路径 1 检查 .so/.dex 为 deflate 压缩而非 store 破解工具在重打包时未按 Android 规范处理 5 platform 重签后 App 闪退 运行时 AndyHax Hook 框架 native 初始化 HookApplication.b() 抛出 NPE，签名变化导致 native 层取不到预期数据 破解注入框架依赖原始签名做 native 层初始化 6 APK 原封不动预置到 /product/app/，签名完整，App 仍崩溃 运行时 LOCAL_REPLACE_PREBUILT_APK_INSTALLED 纯拷贝 APK 到 /product/app/，签名保留完整。但该路径跳过了 JNI 提取流程（app_prebuilt_internal.mk L185-188 在 install_jni_libs.mk 之后，直接绕过所有处理），APK 内的 .so 未被提取到 APK 旁的 lib 目录。运行时 linker 找不到 .so，抛出 UnsatisfiedLinkError: dlopen failed: library \u0026quot;libRTXApp.so\u0026quot; not found LOCAL_REPLACE_PREBUILT_APK_INSTALLED 只拷贝 APK，不提取 JNI。/product/ 是只读分区，PMS 没有写权限，无法在运行时将 APK 内的 .so 解压出来——与 adb install 到 /data/app/ 不同，/data/ 可读写，PMS 能正常解压 7 提取全部 .so 到 /product/app/TiviMateCustom/lib/ 后 App 仍崩溃 运行时 通过 LOCAL_PREBUILT_JNI_LIBS_* + extract_libs.sh 将 26 个 .so 编译期提取并随镜像打包到 APK 旁的 lib 目录。linker 能正常加载（shadowhook/bytehook 初始化成功），但崩溃点变为 HookApplication.b(Native Method) NPE .so 加载问题已解决，但 AndyHax 的 HookApplication.\u0026lt;clinit\u0026gt;() static 初始化块调用 native 方法时，native 层检测到进程以 system app 身份（system UID + system_app SELinux 域）运行而返回 NPE。该框架设计时假定运行在 data app 上下文，不兼容 system app 的进程模型——详见第四章进阶分析 8 init.rc 配置正确但首启脚本未执行 首启 on property:sys.boot_completed=1 trigger 未触发 /vendor/etc/init/*.rc 加载时机晚于 sys.boot_completed=1 的设置时机。Property trigger 只在属性值变化为目标值的瞬间触发，没有重放机制——rc 被解析时 boot_completed 已经是 1，没有\u0026quot;变化\u0026quot;发生，trigger 永远不亮。改用 on boot（init 阶段事件，有重放保证）解决 9 改用 on boot 后脚本仍不执行，被 SELinux 拦截 首启 dmesg 显示 init: Could not start service 'tivimate-install' as part of class 'main': File /vendor/bin/install_tivimate.sh (labeled \u0026quot;u:object_r:vendor_file:s0\u0026quot;) has incorrect label or no domain transition from u:r:init:s0 to another SELinux domain defined 脚本文件缺少 SELinux 标签。init 执行脚本时需要从 init 域切换到脚本对应的域，但 vendor_file 没有定义域转换规则。需要：1) file_contexts 中给脚本打特定标签（tivimate_install_exec）；2) 创建 .te 文件定义域类型（tivimate_install）并用 init_daemon_domain 建立 init→域的转换——详见 3.md 10 APK 在 /vendor/etc/tivimate/ 下但文件名缺了 .apk 后缀，脚本找不到 首启 BUILD_PREBUILT + ETC class 时，安装文件名取自 LOCAL_MODULE（而非 LOCAL_SRC_FILES）。LOCAL_MODULE := TiviMateCustom 导致设备上文件名为 TiviMateCustom（无后缀），脚本中写的路径是 TiviMateCustom.apk，[ ! -f \u0026quot;$APK\u0026quot; ] 命中直接 abort ETC 模块的文件名=模块名。需要 LOCAL_MODULE := TiviMateCustom.apk（含后缀），同时 device-giec.mk 中 PRODUCT_PACKAGES 也要对应更新为 TiviMateCustom.apk 四、进阶分析 4.1 构建系统死锁 ┌─── PRESIGNED + SDK \u0026gt; 29 │ ↓ │ 路径 1: do_not_alter_apk │ L212 check-jni-dex-compression → ❌ .so/.dex 压缩 │ 无变量可跳过（定义在 definitions.mk:3011） │ TiviMateCustom.apk┤ (.so/.dex 压缩) │ │ PRESIGNED + 无 SDK_VERSION │ ↓ │ 路径 2: uncompress + align │ 签名全废 → ❌ INSTALL_PARSE_FAILED │ │ platform 重签 → ❌ AndyHax native 层 NPE 崩溃 │ │ 手动 zip2zip + zipalign → ❌ 签名全废 根本矛盾：.so/.dex 规范（store）和签名不可兼得。从外部修改 → 废签名；保留签名 → 过不了规范检查；换签名 → AndyHax 注入框架 native 初始化时校验签名失败（HookApplication.b() NPE）。唯一能同时满足规范和签名的角色是原始打包者。\n4.2 为什么 PMS 安装到 /data/app/ 能工作，/product/app/ 不行 关键差异在于 分区可写性。\nadb install / pm install 到 /data/app/ 的流程：\npm install → PMS 接收 APK → 签名验证（原版签名，通过） → 在 /data/app/ 创建安装目录（可读写） → 解压 APK 内压缩的 .so 到 /data/app/\u0026lt;pkg\u0026gt;/lib/arm/ → 为 app 分配独立 UID（u0_aXXX） → App 以 data app 上下文运行 → AndyHax native 正常初始化 ✅ 系统预置到 /product/app/ 的流程：\n编译期: BUILD_PREBUILT → APK 拷贝到 /product/app/（只读分区） JNI .so 留在 APK 内，未被提取 开机: PMS 扫描 /product/app/ → 注册为系统应用 App 启动 → linker 需要加载 .so → .so 在 APK 内是压缩的（deflate），不能 mmap 直接映射 → 需要解压到 lib 目录，但 /product/ 只读，PMS 无写权限 → UnsatisfiedLinkError: dlopen failed ❌ PMS 在安装时有一步 extractNativeLibs 操作，需要将 APK 内的压缩 .so 解压到 app 专属的 native library 目录。对于 /data/app/，该目录在可读写分区上；对于 /product/app/，该目录在只读分区上，PMS 没有写入权限。\n4.3 .so 预提取方案的尝试与结果 为绕过 /product/ 只读限制，我们尝试了编译期预提取 .so 的方案：\n方案设计：\n编译期通过 extract_libs.sh 从 APK 中提取压缩的 .so（共 26 个） 通过 LOCAL_PREBUILT_JNI_LIBS_* 将 .so 随镜像打包到 /product/app/TiviMateCustom/lib/arm/ 和 lib/arm64/ 利用 install_jni_libs_internal.mk 的 copy-one-file（无 ELF 检查）避开 PRODUCT_COPY_FILES 的限制 结果：native 库加载成功，但 APP 仍然崩溃。\nlogcat 关键信息: D nativeloader: Configuring product-clns-4 for unbundled product apk library_path=/product/app/TiviMateCustom/lib/arm:... ← linker 能找到 .so FATAL EXCEPTION: main java.lang.ExceptionInInitializerError Caused by: java.lang.NullPointerException: NullPointerException at com.andyhax.hook.HookApplication.b(Native Method) ← 不是 .so 找不到 这说明两个事实：\n.so 预提取方案在技术上完全正确——linker 已成功加载 shadowhook/bytehook 等 native 库 崩溃不是因为 .so 缺失，而是 AndyHax 的 HookApplication.\u0026lt;clinit\u0026gt;() native 初始化在检测到进程上下文异常后主动返回 NPE 为什么 system app 上下文会导致 native 初始化失败：\n维度 Data App (/data/app/) System App (/product/app/) UID 独立分配的 u0_aXXX（≥10000） 共享 system UID（1000）或 product 分区 UID SELinux 域 untrusted_app system_app 或 platform_app ClassLoader 标准 app ClassLoader product-clns-4 ClassLoader 配置 数据目录 /data/data/\u0026lt;pkg\u0026gt;/（完整读写） 理论上同路径，但权限可能不完整 AndyHax 需要在 native 层执行进程注入、PLT Hook、内存操作等。其中对 UID 范围的检查是最直接的失败点——native 代码可能做了 uid \u0026lt; 10000 → return null 这样的判断来防止注入系统进程。\n这解释了为什么同一套 .so 和同一个 APK（未修改签名）在 /data/app/ 能跑，在 /product/app/ 就崩溃。\n4.4 init.rc 执行时机陷阱 第三个陷阱在 init.rc 配置阶段暴露。\n常规认知： on property:sys.boot_completed=1 在开机完成后触发，适合执行需要 PMS 就绪的操作。\n实际情况： 脚本没有执行。\n原因： Property trigger 只在属性值变化的瞬间触发，没有重放机制。Android init 的 rc 加载顺序是：\n/system/etc/init/*.rc → 较早 /vendor/etc/init/*.rc → 较晚，可能晚于 boot_completed 当 /vendor/etc/init/init.tivimate.rc 被解析时，sys.boot_completed 可能已经是 1。属性值没有\u0026quot;变化\u0026quot;，trigger 永远不亮。\n解决方案： 改用 on boot（init 自身阶段事件）。阶段事件有重放保证——即使 rc 加载晚于事件发射，init 检测到事件已发生会立即执行对应的 action。脚本内部用 while 等待 boot_completed 弥补阶段过早的问题，两者互补。\n4.5 各阶段崩溃证据（logcat） platform 重签崩溃 FATAL EXCEPTION: main Process: ar.tvplayer.tv, PID: 2477 java.lang.ExceptionInInitializerError at ar.tvplayer.tv.ProtectedAppComponentFactory.instantiateApplication(Unknown Source:4) at android.app.Instrumentation.newApplication(Instrumentation.java:1282) at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1467) Caused by: java.lang.NullPointerException: NullPointerException at com.andyhax.hook.HookApplication.b(Native Method) at com.andyhax.hook.HookApplication.\u0026lt;clinit\u0026gt;(Unknown Source:8) 崩溃链路：ProtectedAppComponentFactory → HookApplication.\u0026lt;clinit\u0026gt;() → HookApplication.b() native → NPE。签名变化导致 AndyHax native 层取不到预期数据。\n纯拷贝（LOCAL_REPLACE_PREBUILT_APK_INSTALLED）崩溃 — .so 缺失 FATAL EXCEPTION: main java.lang.UnsatisfiedLinkError: dlopen failed: library \u0026#34;libRTXApp.so\u0026#34; not found at java.lang.System.loadLibrary(System.java:1661) at rtx.app.RTXRebrand.\u0026lt;clinit\u0026gt;(Unknown Source:2) APK 签名完整，但 JNI 未被提取，.so 压缩在 APK 内，/product/ 只读导致 PMS 无法解压。\n.so 预提取后崩溃 — system app 上下文不兼容 FATAL EXCEPTION: main java.lang.ExceptionInInitializerError Caused by: java.lang.NullPointerException: NullPointerException at com.andyhax.hook.HookApplication.b(Native Method) at com.andyhax.hook.HookApplication.\u0026lt;clinit\u0026gt;(Unknown Source:8) 此处的 NPE 不是签名问题（签名始终未改），而是 native 层检测到 system app 上下文（UID/SELinux 域）不符合预期，主动返回 null。\n五、解决方案 5.1 核心思路 AndyHax 注入框架的 native 层只能在 data app 上下文（独立 UID、untrusted_app SELinux 域）中正常初始化。系统预置到 /product/app/ 会使其以 system UID 运行，native 初始化返回 NPE。\n绕过方式：编译期将 APK 作为普通数据文件（LOCAL_MODULE_CLASS := ETC）放到 /vendor/etc/tivimate/，PMS 不会扫描非 app 目录，不会注册为系统应用。首启时通过 init.rc oneshot 服务调用 pm install -r 将其安装到 /data/app/。PMS 安装后为 app 分配独立 UID，AndyHax 正常运行。后续启动通过 flag + pm list packages 双重校验跳过，零开销。\n5.2 文件配置 Android.mk 文件: vendor/giec/apps/TiviMateCustom/Android.mk\nLOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := TiviMateCustom.apk LOCAL_SRC_FILES := TiviMateCustom.apk # Ship as a plain data file (not an app module) so PMS does not auto-register it. # The init.rc oneshot service will pm install it to /data/app/ on first boot. LOCAL_MODULE_CLASS := ETC LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR)/etc/tivimate include $(BUILD_PREBUILT) 关键设计决策：为什么用 ETC 而非 APPS\n方式 PMS 行为 AndyHax APPS → /product/app/ 开机扫描 → 注册为系统应用 → system UID ❌ NPE ETC → /vendor/etc/tivimate/ 不扫描，仅是一般文件 ✅ 等脚本 pm install → /data/app/ ETC class 把 APK 当作普通数据文件处理，不走 APK 构建流水线（无需签名声明、dexpreopt、uses-library 校验），且 /vendor/etc/ 不在 PMS 扫描路径内，不会触发系统应用注册。唯一的注意事项是 ETC 模块的安装文件名取自 LOCAL_MODULE，因此模块名必须包含 .apk 后缀才能与脚本路径一致。\ndevice-giec.mk（相关片段） 文件: vendor/giec/device-giec.mk\n########################################################################### # TiviMateCustom — third-party IPTV player APK (must preserve signature). # APK is shipped to /vendor/etc/tivimate/ as a plain data file (ETC module) # so PMS does not auto-register it. On first boot an init.rc oneshot service # runs pm install -r to install it to /data/app/ (required by AndyHax). PRODUCT_PACKAGES += TiviMateCustom.apk PRODUCT_COPY_FILES += \\ vendor/giec/executable/install_tivimate.sh:vendor/bin/install_tivimate.sh \\ vendor/giec/common/init.tivimate.rc:vendor/etc/init/init.tivimate.rc init.rc 文件: vendor/giec/common/init.tivimate.rc\n# First-boot installer for TiviMateCustom APK. # Runs once after boot completes and installs the APK as a data app. # Triggered by on-boot (the script internally waits for boot_completed). service tivimate-install /vendor/bin/install_tivimate.sh class main user root group root system oneshot disabled on boot start tivimate-install 为什么使用 on boot 而非 on property:sys.boot_completed=1：\nAndroid init 的 property trigger 只在属性值变化为目标值的瞬间触发，没有重放机制。/vendor/etc/init/*.rc 的加载时机晚于 sys.boot_completed=1 的设置时机——rc 被解析时 boot_completed 已经是 1，没有\u0026quot;变化\u0026quot;发生，trigger 永远不亮。\non boot 是 init 自身的阶段事件，有重放保证——即使 rc 加载晚于 boot 事件，init 检测到 boot 已发生会立即执行 action。脚本内部通过 while [ \u0026quot;$(getprop sys.boot_completed)\u0026quot; != \u0026quot;1\u0026quot; ] 等待 PMS 就绪，弥补了 boot 阶段过早的问题。\n首启安装脚本 文件: vendor/giec/executable/install_tivimate.sh\n#!/system/bin/sh # First-boot installer for TiviMateCustom APK. PKG=\u0026#34;ar.tvplayer.tv\u0026#34; APK=\u0026#34;/vendor/etc/tivimate/TiviMateCustom.apk\u0026#34; FLAG=\u0026#34;/data/local/tmp/.tivimate_installed\u0026#34; # Already installed — nothing to do. if [ -f \u0026#34;$FLAG\u0026#34; ] \u0026amp;\u0026amp; pm list packages \u0026#34;$PKG\u0026#34; 2\u0026gt;/dev/null | grep -q \u0026#34;^package:$PKG$\u0026#34;; then exit 0 fi # If flag exists but package is gone (user uninstalled), remove stale flag so we retry. if [ -f \u0026#34;$FLAG\u0026#34; ]; then rm \u0026#34;$FLAG\u0026#34; fi if [ ! -f \u0026#34;$APK\u0026#34; ]; then log -t install_tivimate -p e \u0026#34;$APK not found, aborting\u0026#34; exit 1 fi # Wait for package manager to be ready (timeout 120s to avoid infinite loop). MAX_WAIT=120 waited=0 while [ \u0026#34;$(getprop sys.boot_completed)\u0026#34; != \u0026#34;1\u0026#34; ]; do sleep 1 waited=$((waited + 1)) if [ \u0026#34;$waited\u0026#34; -ge \u0026#34;$MAX_WAIT\u0026#34; ]; then log -t install_tivimate -p e \u0026#34;boot_completed not set after ${MAX_WAIT}s, aborting\u0026#34; exit 1 fi done sleep 2 log -t install_tivimate -p i \u0026#34;installing TiviMateCustom...\u0026#34; # Retry up to 3 times — cold boot on slow storage may delay PMS readiness. for i in 1 2 3; do pm install -r \u0026#34;$APK\u0026#34; \u0026amp;\u0026amp; touch \u0026#34;$FLAG\u0026#34; \u0026amp;\u0026amp; break sleep 3 done if [ -f \u0026#34;$FLAG\u0026#34; ]; then log -t install_tivimate -p i \u0026#34;done\u0026#34; else log -t install_tivimate -p e \u0026#34;failed after 3 retries, will retry on next boot\u0026#34; exit 1 fi 防重复安装机制：\nflag 文件 + pm list packages 双重校验，flag 可被伪造但包列表来自 PMS，更可靠 用户卸载后 flag 残留 → 检测到包不存在 → 删 flag → 重新安装（自动恢复） pm install 成功后 \u0026amp;\u0026amp; touch 创建标记，失败不创建 → 下次启动重试 启动等待带 120s 超时，避免异常启动场景死循环 pm install 带 3 次重试，间隔 3s，应对慢存储冷启动 日志通过 log -t install_tivimate 进 logcat，field 问题可定位 5.3 完整工作流程 编译期: BUILD_PREBUILT (ETC class) → 拷贝 APK → /vendor/etc/tivimate/TiviMateCustom.apk（普通文件，PMS 不扫描） PRODUCT_COPY_FILES → install_tivimate.sh → /vendor/bin/ → init.tivimate.rc → /vendor/etc/init/ 首启时: init second_stage → 解析 /vendor/etc/init/init.tivimate.rc → on boot 触发 → SELinux 域转换（init → tivimate_install） → install_tivimate.sh 执行: 1. flag 存在 \u0026amp;\u0026amp; pm list packages 查到包 → 跳过（双重校验） 2. flag 存在但包被卸载 → 删 flag → 继续安装（自动恢复） 3. APK 存在性检查 → 不存在则 abort 4. while boot_completed != 1 → sleep 1（120s 超时） 5. pm install -r（最多 3 次重试）→ PMS 安装到 /data/app/ → 分配独立 UID → AndyHax 正常初始化 6. install 成功 → touch flag 后续启动: init → on boot → start tivimate-install → flag ✓ + 包 ✓ → exit 0（零开销） 5.4 方案总结 对比项 纯系统预置（APPS module → /product/app/） 本方案（ETC → /vendor/etc/tivimate/ + 首启 pm install） 构建模块类型 APPS（PMS 开机扫描注册） ETC（普通文件，PMS 不扫描） 编译期处理 需声明签名/dexpreopt/uses-library，且面临压缩检查死锁 无，纯文件拷贝 运行时 UID system UID 独立 u0_aXXX SELinux 域 system_app untrusted_app AndyHax 兼容 ❌ HookApplication.b() NPE ✅ 正常 手动介入 需要 零介入 APK 存储 仅 /product/app/ 一份 /vendor/etc/tivimate/（安装源）+ /data/app/（运行时）两份 六、排查工具速查 # ---------- 签名 ---------- # 查签名方案 apksigner verify --verbose --print-certs xxx.apk # ---------- 压缩方式 ---------- # 查 .so/.dex 压缩状态 zipinfo xxx.apk \u0026#39;lib/*/*.so\u0026#39; \u0026#39;*.dex\u0026#39; | grep -v \u0026#39; stor \u0026#39; # 对比两个 APK 的压缩方式差异 python3 -c \u0026#34; import zipfile for label, path in [(\u0026#39;A\u0026#39;, \u0026#39;a.apk\u0026#39;), (\u0026#39;B\u0026#39;, \u0026#39;b.apk\u0026#39;)]: z = zipfile.ZipFile(path) total_orig = sum(info.file_size for info in z.infolist()) total_comp = sum(info.compress_size for info in z.infolist()) methods = {} for info in z.infolist(): m = {0:\u0026#39;store\u0026#39;, 8:\u0026#39;deflate\u0026#39;}.get(info.compress_type, str(info.compress_type)) methods[m] = methods.get(m, 0) + 1 print(f\u0026#39;{label}: 文件数={len(z.infolist())}, 原始={total_orig}, 压缩={total_comp}, {methods}\u0026#39;) z.close() \u0026#34; # ---------- 对齐 ---------- # 查 4 字节对齐 zipalign -c -p 4 xxx.apk # ---------- manifest ---------- # 查包名 + SDK 版本 aapt dump badging xxx.apk | grep -E \u0026#34;(package:|launchable-activity:)\u0026#34; # 查 uses-library 声明 aapt dump badging xxx.apk | grep uses-library # 查 compileSdkVersion aapt dump badging xxx.apk | grep compileSdkVersion # ---------- 破解检测 ---------- # 查可疑注入类 unzip -p xxx.apk classes*.dex | strings | grep -iE \u0026#34;killer|hack|signaturertx|rebrand|hook|xposed|hiddenapibypass\u0026#34; | sort -u # 查签名持有者 apksigner verify --verbose --print-certs xxx.apk | grep \u0026#34;certificate DN\u0026#34; # ---------- 首启安装调试 ---------- # 查看 init 相关日志 dmesg | grep -i \u0026#34;init\u0026#34; # 查看安装脚本日志 logcat -d | grep -i \u0026#34;install_tivimate\u0026#34; # 查看 PMS 安装日志 logcat -d | grep -i \u0026#34;tivimate\u0026#34; # 手动测试安装 sh /vendor/bin/install_tivimate.sh # 清除安装标记（强制下次启动重装） rm /data/local/tmp/.tivimate_installed 七、推荐预置 APK 检查清单 # 检查项 命令 通过条件 1 签名完整 (v1+v2+v3) apksigner verify --verbose 三项 true 2 .so/.dex store zipinfo 'lib/*/*.so' '*.dex' | grep -v ' stor ' 无输出 3 4 字节对齐 zipalign -c -p 4 exit 0 4 uses-library 声明 aapt dump badging | grep uses-library 补到 Android.mk 5 compileSdkVersion aapt dump badging | grep compileSdkVersion 写入 LOCAL_SDK_VERSION 6 无注入框架类 strings classes.dex | grep -iE \u0026quot;killer|hack|signaturertx\u0026quot; 无输出 7 签名者身份 apksigner verify --print-certs | grep DN 匹配预期开发者 注：对于已确认包含注入框架的破解 APK（如本案例），第 2、6 项无法满足。此时检查清单仅作为问题识别之用，实际预置方案见第五章。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/%E9%A2%84%E7%BD%AEapk/","summary":"\u003ch3 id=\"1-目录结构扫描与功能说明\"\u003e\u003cstrong\u003e1. 目录结构扫描与功能说明\u003c/strong\u003e\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003e一级目录\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e二级目录 (关键)\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e作用说明\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003eAPP 相关标注\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eamlogic/\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003ecommon/apps/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eAmlogic 通用第三方或预置 APP (如 Netflix)\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eAPP 源码/APK 存放地\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003ereference/apps/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eAmlogic 参考应用 (如 TVInput, DLNA)\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eAPP 源码存放地\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003ecommon/sepolicy/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eAmlogic 通用 SELinux 策略\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e权限配置\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003egiec/\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003eapps/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eGIEC 厂商自定义 APP (如 Glauncher, OTAClient)\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e核心 APP 存放地\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003ecommon/sepolicy/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eGIEC 厂商自定义 SELinux 策略\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e权限配置\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003eandroid-certs/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e厂商签名密钥 (platform, shared 等)\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e签名资源\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003eexecutable/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e厂商自定义脚本与二进制工具\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e运行辅助\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ewidevine/\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003elibwvdrmengine/\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eWidevine DRM 相关组件\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e辅助组件\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch3 id=\"2-构建文件-androidmkandroidbp-分析\"\u003e\u003cstrong\u003e2. 构建文件 (Android.mk/Android.bp) 分析\u003c/strong\u003e\u003c/h3\u003e\n\u003ch4 id=\"1-预置-apk-模式-以-glauncher-为例\"\u003e\u003cstrong\u003e(1) 预置 APK 模式 (以 Glauncher 为例)\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003e在 \u003ca href=\"\"\u003eGlauncher/Android.mk\u003c/a\u003e 中：\u003c/p\u003e","title":"预置APK"},{"content":"Android 系统 Build 阶段 APK 签名机制（通俗笔记） 面向：刚接触 AOSP/系统应用开发、需要“系统签名 / system 权限”概念的人\n目标：用更直白的方式理解 AOSP 在编译（build）时如何给 APK 签名，以及 如何自定义签名 key、如何让 APK 以 system UID 运行。\n原文参考：https://maoao530.github.io/2017/01/31/android-build-sign/\n0. 你需要先知道的 3 个概念 签名是干什么的？\nAndroid 用签名来确认“这个 APK 是谁发布/构建的”，并据此决定：\n是否允许共享 UID（sharedUserId） 是否允许同签名应用间共享数据/权限 系统应用/特权能力是否能被授予（取决于场景与版本策略） 非对称加密（公钥/私钥）一句话版\n私钥：必须保密，用来“签名”。 公钥：可以公开，用来“验签”。 Build 阶段签名 ≠ Android Studio 打包签名\n这里讲的是 AOSP 源码编译（make/soong）时，系统在构建流程中对 APK 进行签名。 Android Studio 的 keystore/gradle 签名属于另一条链路，但原理相通。 1. AOSP 默认有哪些签名 Key？ 系统源码里默认准备了 4 组 key，用于在 build 阶段给不同类型的 APK 签名。它们默认位于：\nbuild/target/product/security/\n每组 key 都有两类文件：\n*.pk8：私钥（用于签名） *.x509.pem：公钥证书（用于验签） key 名称 私钥文件 公钥文件 常见用途（通俗理解） testkey testkey.pk8 testkey.x509.pem 默认普通 APK（不指定时用它） platform platform.pk8 platform.x509.pem 系统核心/平台相关 APK（常用于 system UID） shared shared.pk8 shared.x509.pem 与共享 UID / 共享权限相关的一类系统组件 media media.pk8 media.x509.pem media/download 等相关组件 记忆方法：\ntestkey = 默认；platform = 系统平台核心；shared/media 是系统特定组件链路会用到的 key。\n2. Build 时到底用哪把 key 给 APK 签名？ 2.1 由 Android.mk 的 LOCAL_CERTIFICATE 决定 在 APK 的 Android.mk（或对应构建脚本）里，通过 LOCAL_CERTIFICATE 指定签名类型：\nLOCAL_CERTIFICATE := testkey # 普通 APK（默认） LOCAL_CERTIFICATE := platform # 系统核心能力相关（常见：system UID） LOCAL_CERTIFICATE := shared # 系统链路组件 LOCAL_CERTIFICATE := media # 系统链路组件 如果你不写 LOCAL_CERTIFICATE，默认会使用 testkey。\n2.2 默认签名 key 的兜底逻辑（config.mk） build/core/config.mk 中有默认值（原文核心片段如下）：\n# The default key if not set as LOCAL_CERTIFICATE ifdef PRODUCT_DEFAULT_DEV_CERTIFICATE DEFAULT_SYSTEM_DEV_CERTIFICATE := $(PRODUCT_DEFAULT_DEV_CERTIFICATE) else DEFAULT_SYSTEM_DEV_CERTIFICATE := build/target/product/security/testkey endif 含义（通俗翻译）：\n厂商如果在产品配置里设置了 PRODUCT_DEFAULT_DEV_CERTIFICATE，那就用厂商指定的默认 key 路径； 否则就用 AOSP 自带的 testkey。 3. 为什么“系统签名”常常还要配 sharedUserId？ 很多同学的目标其实是：让 APK 具备 system 权限/以 system UID 运行。\n在传统 AOSP 体系里，一个常见组合是：\nAndroidManifest.xml 中声明： \u0026lt;manifest ... android:sharedUserId=\u0026#34;android.uid.system\u0026#34;\u0026gt; Android.mk 中使用 platform key： LOCAL_CERTIFICATE := platform 这样重新编译后，应用进程的 UID 可能会变成 system（可用 ps 等方式查看）。\n注意：不同 Android 版本、不同 ROM/安全策略（SELinux、privapp-permissions、签名白名单等）会影响“是否真能拿到你想要的能力”。\n但从本文的 build 签名角度，核心就是：manifest 的 sharedUserId + 匹配的签名。\n4. Build 阶段签名流程（图解） 下面用一个“编译→选择 key→签名→产物”的简化流程图帮助理解：\nflowchart TD A[编译 APK 目标] --\u0026gt; B{Android.mk/构建脚本\\n是否设置 LOCAL_CERTIFICATE?} B --否--\u0026gt; C[使用 DEFAULT_SYSTEM_DEV_CERTIFICATE\\n默认值] B -- 是 --\u0026gt; D[按 LOCAL_CERTIFICATE\\n 手动指定] C --\u0026gt; E[使用 *.pk8 私钥进行签名] D --\u0026gt; E[使用 *.pk8 私钥进行签名] E --\u0026gt; F[输出已签名 APK] 5. 如何自定义系统签名 key（releasekey 等） 目的：不用 AOSP 默认 key，而是生成“你们自己的”平台 key（更符合量产/安全要求）。\n原文给出的做法是使用 Development/tools/ 下的 make_key 工具生成 key。\n5.1 进入工具目录 5.2 使用 make_key 生成一组 key 示例（生成名为 releasekey 的一组 key）：\nsh make_key releasekey \u0026#39;/C=CN/ST=Guangdong/L=Shenzhen/O=Mediatek/OU=MTK/CN=fzll/emailAddress=maoao530@foxmail.com\u0026#39; 参数解释（证书 DN 信息）：\nC：国家（2 位代码） ST：省/州 L：城市 O：组织/公司 OU：部门 CN：通用名（人名/服务器名） emailAddress：邮箱 5.3 生成结果是什么？ 你会看到多出两个文件：\nreleasekey.x509.pem releasekey.pk8 （原文截图）\n5.4 生成 platform/shared/media 等其它 key 同样步骤分别生成 platform / shared / media 对应的 key（做法与上面一致）。\n5.5 替换系统默认 key 将你自定义生成的 key 替换 到：\nbuild/target/product/security/\n之后再次编译，系统 build 阶段就会使用你替换后的 key 来签名产物。\n6. 如何对 APK 进行“系统签名”（通俗步骤） 如果你的目标是“让 APK 具备 system 相关能力”，常见步骤（对应原文第 3 节）：\n在 AndroidManifest.xml 的 \u0026lt;manifest\u0026gt; 上加： android:sharedUserId=\u0026#34;android.uid.system\u0026#34; 在 Android.mk 中加： LOCAL_CERTIFICATE := platform 重新编译（产物为已签名 APK），并通过进程 UID 观察效果（例如 ps）。 7. 常见坑与注意事项（强烈建议读） 私钥（.pk8）一定要保密\n一旦泄露，别人就能伪造“同签名 APK”，后果严重。\n不要在量产设备继续用 AOSP 默认 testkey/platform\n这些 key 是公开的，安全性极差。量产必须替换为厂商自有 key。\nsharedUserId 在新版本 Android 中限制更多\nAndroid 逐步收紧 sharedUserId 的使用，升级系统时需要额外评估兼容性与策略要求。\n“系统签名”不等于自动拥有所有特权\n还可能需要配套：\n作为 /system/priv-app 安装 privapp-permissions 白名单 SELinux domain / sepolicy 配置等 8. 一句话总结 AOSP build 阶段会根据 LOCAL_CERTIFICATE（或默认值）选择 key，在编译过程中对 APK 进行签名。 系统默认提供 testkey/platform/shared/media 四组 key；厂商量产应生成并替换自有 key。 想让 APK 以 system UID 运行，常见组合是：sharedUserId=android.uid.system + LOCAL_CERTIFICATE := platform（并配合系统策略）。 ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/%E7%AD%BE%E5%90%8D%E7%9B%B8%E5%85%B3/","summary":"\u003ch1 id=\"android-系统-build-阶段-apk-签名机制通俗笔记\"\u003eAndroid 系统 Build 阶段 APK 签名机制（通俗笔记）\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e面向：刚接触 AOSP/系统应用开发、需要“系统签名 / system 权限”概念的人\u003cbr\u003e\n目标：用更直白的方式理解 \u003cstrong\u003eAOSP 在编译（build）时如何给 APK 签名\u003c/strong\u003e，以及 \u003cstrong\u003e如何自定义签名 key\u003c/strong\u003e、\u003cstrong\u003e如何让 APK 以 system UID 运行\u003c/strong\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e原文参考：\u003ca href=\"https://maoao530.github.io/2017/01/31/android-build-sign/\"\u003ehttps://maoao530.github.io/2017/01/31/android-build-sign/\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"0-你需要先知道的-3-个概念\"\u003e0. 你需要先知道的 3 个概念\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e签名是干什么的？\u003c/strong\u003e\u003cbr\u003e\nAndroid 用签名来确认“这个 APK 是谁发布/构建的”，并据此决定：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e是否允许共享 UID（sharedUserId）\u003c/li\u003e\n\u003cli\u003e是否允许同签名应用间共享数据/权限\u003c/li\u003e\n\u003cli\u003e系统应用/特权能力是否能被授予（取决于场景与版本策略）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e非对称加密（公钥/私钥）一句话版\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e私钥\u003c/strong\u003e：必须保密，用来“签名”。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e公钥\u003c/strong\u003e：可以公开，用来“验签”。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eBuild 阶段签名 ≠ Android Studio 打包签名\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e这里讲的是 \u003cstrong\u003eAOSP 源码编译\u003c/strong\u003e（make/soong）时，系统在构建流程中对 APK 进行签名。\u003c/li\u003e\n\u003cli\u003eAndroid Studio 的 keystore/gradle 签名属于另一条链路，但原理相通。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-aosp-默认有哪些签名-key\"\u003e1. AOSP 默认有哪些签名 Key？\u003c/h2\u003e\n\u003cp\u003e系统源码里默认准备了 \u003cstrong\u003e4 组 key\u003c/strong\u003e，用于在 build 阶段给不同类型的 APK 签名。它们默认位于：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ebuild/target/product/security/\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e每组 key 都有两类文件：\u003c/p\u003e","title":"Android 系统 Build 阶段 APK 签名机制（通俗笔记）"},{"content":"开机LOGO\nZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\device\\amlogic\\ross\\logo_img_files OR Z:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\device\\amlogic\\common\\logo_img_files 取决于TARGET_AMLOGIC_RES_PACKAGE的值\nZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\device\\amlogic\\planck\\BoardConfig.mk 开机动画路径定义以及查找优先级相关都在这个文件内\nZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\frameworks\\base\\cmds\\bootanimation\\BootAnimation.cpp ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/%E6%9B%B4%E6%8D%A2%E5%BC%80%E6%9C%BA%E5%8A%A8%E7%94%BBlogo/","summary":"\u003cp\u003e开机LOGO\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\device\\amlogic\\ross\\logo_img_files\nOR\nZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\device\\amlogic\\common\\logo_img_files\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e取决于TARGET_AMLOGIC_RES_PACKAGE的值\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\device\\amlogic\\planck\\BoardConfig.mk\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e开机动画路径定义以及查找优先级相关都在这个文件内\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\frameworks\\base\\cmds\\bootanimation\\BootAnimation.cpp\n\u003c/code\u003e\u003c/pre\u003e","title":"更换开机动画LOGO"},{"content":"Android Init 开机脚本编写指南 以 TiviMateCustom 首启自动安装为例，覆盖 init.rc 编写、shell 脚本编写、SELinux 策略和调试全流程。\n一、Android Init 基础 1.1 Init 进程的阶段事件 Android init（PID 1）按固定阶段推进，每个阶段发射一个不可重复的事件：\nfirst_stage └── selinux_setup └── second_stage ├── early-init ├── init ├── late-init │ ├── early-fs │ ├── fs │ ├── post-fs │ ├── late-fs │ └── post-fs-data ├── early-boot ├── boot ← 文件系统就绪，核心服务准备启动 └── ... （Zygote、system_server 等启动） 运行时: sys.boot_completed=1 ← 开机动画结束，Launcher 就绪 关键区别：\n事件类型 触发方式 重放机制 适用场景 阶段事件（on boot, on late-init 等） init 内部发射 有 — rc 晚加载也会追溯执行 不依赖属性的初始化 property trigger（on property:xxx=y） 属性值变化 无 — 错过就永远错过 rc 必须比属性变化早加载 1.2 RC 文件的加载路径 Init 自动 import 以下目录的 .rc 文件：\n/system/etc/init/*.rc ← 较早（system 分区） /vendor/etc/init/*.rc ← 较晚（vendor 分区） /odm/etc/init/*.rc /product/etc/init/*.rc ← 可能更晚 重要： 分区 rc 的加载顺序和时机不固定。vendor/product 分区的 rc 可能晚于某些阶段事件甚至 property trigger。这就是为什么推荐使用 on boot 而非 on property:sys.boot_completed=1。\n二、init.rc 编写 2.1 文件命名 \u0026lt;name\u0026gt;.rc 放在 vendor/giec/common/ 下，通过 PRODUCT_COPY_FILES 拷贝到 /vendor/etc/init/。\n2.2 基本语法 oneshot service（执行一次就退出） service \u0026lt;name\u0026gt; \u0026lt;executable_path\u0026gt; class main # 归属的 class（main / core / late_start） user root # 以哪个用户运行 group root system # 附加组 oneshot # 执行完就退出，不重启 disabled # 不随 class_start 自动启动，等待显式 start on \u0026lt;trigger\u0026gt; start \u0026lt;name\u0026gt; 常见 class：\ncore — 最早启动，设备关键服务 main — 核心系统服务就绪后 late_start — 开机完成后 daemon service（常驻后台） service \u0026lt;name\u0026gt; \u0026lt;executable_path\u0026gt; class main user root group root system restart \u0026lt;number\u0026gt; # 退出后重启次数（如 restart 5） on boot start \u0026lt;name\u0026gt; 2.3 选择正确的 trigger Trigger 时机 适用 陷阱 on early-init 最早，mount 之前 极早期初始化 大部分系统服务未就绪 on boot 文件系统就绪、核心服务启动前 开机脚本最佳选择 此时 PMS 未就绪，脚本内部需要等待 on property:sys.boot_completed=1 开机完成、Launcher 就绪 依赖 PMS 的操作 有错过风险（rc 晚加载时不触发） on late-init init 阶段后期 需要在 boot 之前的操作 数据分区可能未挂载 推荐做法： on boot + 脚本内部 while boot_completed != 1 等待。两者互补——trigger 保证一定会执行，脚本内部的等待保证 PMS 已就绪。\n2.4 完整示例 # vendor/giec/common/init.tivimate.rc service tivimate-install /vendor/bin/install_tivimate.sh class main user root group root system oneshot disabled on boot start tivimate-install 三、Shell 脚本编写 3.1 基本要求 #!/system/bin/sh # 使用 /system/bin/sh，不是 /bin/sh 或 /bin/bash # Android 上没有 bash，只有 POSIX sh 3.2 等待开机完成 while [ \u0026#34;$(getprop sys.boot_completed)\u0026#34; != \u0026#34;1\u0026#34; ]; do sleep 1 done # 额外等几秒，确保 PMS、ActivityManager 等完全就绪 sleep 2 3.3 防重复执行（idempotency） 用一个标记文件来判断是否已执行过：\nFLAG=\u0026#34;/data/local/tmp/.my_task_done\u0026#34; if [ -f \u0026#34;$FLAG\u0026#34; ]; then exit 0 fi # 执行任务... pm install -r /path/to/app.apk # 只有成功才写标记，失败时下次启动重试 if [ $? -eq 0 ]; then touch \u0026#34;$FLAG\u0026#34; fi 标记文件位置选择：\n路径 持久性 说明 /data/local/tmp/ 跨重启保留，清 data 后丢失 推荐 — 清除数据后重新安装是预期行为 /data/misc/ 跨重启保留 适合系统级标记 /data/vendor/ vendor 专用持久存储 部分设备支持 3.4 错误处理 pm install -r \u0026#34;$APK\u0026#34; \u0026amp;\u0026amp; touch \u0026#34;$FLAG\u0026#34; if [ -f \u0026#34;$FLAG\u0026#34; ]; then echo \u0026#34;install: done\u0026#34; else echo \u0026#34;install: failed, will retry on next boot\u0026#34; \u0026gt;\u0026amp;2 fi 使用 \u0026amp;\u0026amp; 确保成功才创建标记。即使脚本执行失败，oneshot 服务也不会重试（已退出），但下次启动时标记文件不存在 → 自动重试。\n3.5 完整示例 #!/system/bin/sh # First-boot installer for TiviMateCustom APK. APK=\u0026#34;/product/app/TiviMateCustom/TiviMateCustom.apk\u0026#34; FLAG=\u0026#34;/data/local/tmp/.tivimate_installed\u0026#34; if [ -f \u0026#34;$FLAG\u0026#34; ]; then exit 0 fi while [ \u0026#34;$(getprop sys.boot_completed)\u0026#34; != \u0026#34;1\u0026#34; ]; do sleep 1 done sleep 2 echo \u0026#34;install_tivimate: installing TiviMateCustom...\u0026#34; pm install -r \u0026#34;$APK\u0026#34; \u0026amp;\u0026amp; touch \u0026#34;$FLAG\u0026#34; if [ -f \u0026#34;$FLAG\u0026#34; ]; then echo \u0026#34;install_tivimate: done\u0026#34; else echo \u0026#34;install_tivimate: failed, will retry on next boot\u0026#34; fi 四、SELinux 策略配置 4.1 为什么需要 SELinux 策略 init 执行脚本时，需要从 init 安全域切换到脚本对应的域。如果脚本文件没有专门的 SELinux 标签和域转换规则，init 会拒绝执行：\ninit: Could not start service \u0026#39;xxx\u0026#39; as part of class \u0026#39;main\u0026#39;: File /vendor/bin/xxx.sh (labeled \u0026#34;u:object_r:vendor_file:s0\u0026#34;) has incorrect label or no domain transition from u:r:init:s0 to another SELinux domain defined. 必须同时做两件事：\n给脚本打标签（file_contexts） 建立域转换规则（.te 文件） 4.2 file_contexts — 给文件打标签 文件： vendor/\u0026lt;vendor\u0026gt;/common/sepolicy/file_contexts\n/vendor/bin/\u0026lt;script_name\u0026gt;\\.sh u:object_r:\u0026lt;domain\u0026gt;_exec:s0 示例：\n/vendor/bin/install_tivimate\\.sh u:object_r:tivimate_install_exec:s0 注意：路径中的 . 需要转义为 \\.。\n4.3 .te 文件 — 定义域和转换规则 文件： vendor/\u0026lt;vendor\u0026gt;/common/sepolicy/\u0026lt;domain\u0026gt;.te\n# 1. 声明 domain 类型和 exec 类型 type \u0026lt;domain\u0026gt;, domain; type \u0026lt;domain\u0026gt;_exec, exec_type, vendor_file_type, file_type; # 2. 建立 init → domain 的转换规则 init_daemon_domain(\u0026lt;domain\u0026gt;) # 3. 设为宽容模式（开发阶段推荐） permissive \u0026lt;domain\u0026gt;; 完整示例：\n# vendor/giec/common/sepolicy/tivimate_install.te type tivimate_install, domain; type tivimate_install_exec, exec_type, vendor_file_type, file_type; init_daemon_domain(tivimate_install) permissive tivimate_install; 4.4 关键宏说明 宏 作用 init_daemon_domain(domain) 自动生成 init → domain 的域转换规则 permissive domain 设为宽容模式：允许所有操作，只记录审计日志。调试阶段使用，正式发布应改为显式 allow 规则 4.5 开发步骤 1. 写 .te 文件（定义类型 + init_daemon_domain + permissive） 2. 写 file_contexts 条目 3. 编译，刷机，确认功能正常 4. （可选）根据 audit2allow 生成的日志，用显式 allow 替换 permissive 五、构建系统集成 5.1 device.mk 配置 文件： vendor/\u0026lt;vendor\u0026gt;/device-\u0026lt;vendor\u0026gt;.mk\n# 脚本 → /vendor/bin/ # init.rc → /vendor/etc/init/ PRODUCT_COPY_FILES += \\ vendor/giec/executable/install_tivimate.sh:vendor/bin/install_tivimate.sh \\ vendor/giec/common/init.tivimate.rc:vendor/etc/init/init.tivimate.rc 5.2 编译产物位置 源文件 设备上路径 vendor/giec/executable/xxx.sh /vendor/bin/xxx.sh vendor/giec/common/init.xxx.rc /vendor/etc/init/init.xxx.rc vendor/giec/common/sepolicy/xxx.te 编译进 /vendor/etc/selinux/ 5.3 目录结构总结 vendor/giec/ ├── device-giec.mk # PRODUCT_COPY_FILES 声明 ├── executable/ │ └── install_tivimate.sh # 脚本本体 ├── common/ │ ├── init.tivimate.rc # init 服务定义 │ └── sepolicy/ │ ├── file_contexts # 脚本文件标签 │ └── tivimate_install.te # 域定义和转换规则 └── apps/TiviMateCustom/ └── TiviMateCustom.apk # 待安装的 APK 六、调试方法 6.1 查看 init 是否加载了 rc # 搜索 init 相关日志 dmesg | grep -iE \u0026#34;init.*import|Parsing.*init\u0026#34; 6.2 查看服务启动状态 # 查看 init 是否尝试启动服务，以及是否被 SELinux 拦截 dmesg | grep -i \u0026#34;\u0026lt;service_name\u0026gt;\u0026#34; # 如果 SELinux 拦截，会看到类似： # init: Could not start service \u0026#39;xxx\u0026#39; ... has incorrect label # or no domain transition ... 6.3 查看脚本执行日志 # 脚本中的 echo 输出会出现在 logcat 中 logcat -d | grep -i \u0026#34;install_tivimate\u0026#34; # 或者直接看完整 logcat logcat -d -s install_tivimate 6.4 查看 SELinux 审计日志 # 查看所有 SELinux denials logcat -d | grep \u0026#34;avc.*denied\u0026#34; # 过滤特定域 logcat -d | grep \u0026#34;avc.*denied.*tivimate\u0026#34; 6.5 手动测试 # 确认脚本有执行权限 ls -l /vendor/bin/install_tivimate.sh # 手动跑脚本验证逻辑 sh /vendor/bin/install_tivimate.sh # 重置标记以便重新测试 rm /data/local/tmp/.tivimate_installed # 确认 SELinux 标签已生效 ls -Z /vendor/bin/install_tivimate.sh # 预期输出: u:object_r:tivimate_install_exec:s0 ... 6.6 验证 property getprop sys.boot_completed # 应返回 1 七、常见陷阱速查 # 错误现象 原因 解决 1 脚本完全不执行，dmesg 无任何相关日志 init 没加载 rc 文件 检查 rc 是否拷贝到 /vendor/etc/init/，检查 dmesg 是否有 init import 日志 2 脚本不执行，dmesg 有 Could not start service...has incorrect label SELinux 拦截 1) file_contexts 打标签 2) .te 定义域 + init_daemon_domain 3) permissive 3 脚本执行了但 pm install 失败 PMS 未就绪 脚本中 while boot_completed != 1 + sleep 2 等待 4 on property:sys.boot_completed=1 不触发 rc 加载晚于属性设置 改用 on boot 5 每重启都重新安装 没有防重复机制 用标记文件（/data/local/tmp/）判断 6 脚本看起来跑了但标记文件没生成 pm install 失败，\u0026amp;\u0026amp; 短路 检查 logcat 中 PMS 日志 7 #!/bin/bash 不工作 Android 没有 bash 用 #!/system/bin/sh 八、完整工作流程（本案例总结） 编写阶段: 1. 写脚本 → vendor/giec/executable/install_tivimate.sh 2. 写 rc → vendor/giec/common/init.tivimate.rc 3. 写 SELinux: a. vendor/giec/common/sepolicy/tivimate_install.te (定义域 + init_daemon_domain + permissive) b. vendor/giec/common/sepolicy/file_contexts (脚本打标签) 4. 注册到构建系统 → vendor/giec/device-giec.mk PRODUCT_COPY_FILES: 脚本 + rc 编译: m → APK 预置到 /product/app/ + 脚本/rc/SELinux 策略写入镜像 设备首启: init second_stage → 加载 /vendor/etc/init/init.tivimate.rc → on boot 触发 → start tivimate-install → init → tiviate_install 域转换（SELinux 通过） → install_tivimate.sh: → 检查标记文件 → 不存在 → while boot_completed != 1 等待 → pm install -r → 安装到 /data/app/ → touch 标记文件 → 退出 后续启动: init → on boot → start tivimate-install → 检查标记文件 → 存在 → exit 0 ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E5%BC%80%E6%9C%BA%E8%84%9A%E6%9C%AC/","summary":"\u003ch1 id=\"android-init-开机脚本编写指南\"\u003eAndroid Init 开机脚本编写指南\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e以 TiviMateCustom 首启自动安装为例，覆盖 init.rc 编写、shell 脚本编写、SELinux 策略和调试全流程。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一android-init-基础\"\u003e一、Android Init 基础\u003c/h2\u003e\n\u003ch3 id=\"11-init-进程的阶段事件\"\u003e1.1 Init 进程的阶段事件\u003c/h3\u003e\n\u003cp\u003eAndroid init（PID 1）按固定阶段推进，每个阶段发射一个不可重复的事件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efirst_stage\n    └── selinux_setup\n        └── second_stage\n              ├── early-init\n              ├── init\n              ├── late-init\n              │     ├── early-fs\n              │     ├── fs\n              │     ├── post-fs\n              │     ├── late-fs\n              │     └── post-fs-data\n              ├── early-boot\n              ├── boot              ← 文件系统就绪，核心服务准备启动\n              └── ...               （Zygote、system_server 等启动）\n                                    \n              运行时:\n              sys.boot_completed=1  ← 开机动画结束，Launcher 就绪\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003e关键区别：\u003c/strong\u003e\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e事件类型\u003c/th\u003e\n          \u003cth\u003e触发方式\u003c/th\u003e\n          \u003cth\u003e重放机制\u003c/th\u003e\n          \u003cth\u003e适用场景\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e阶段事件（\u003ccode\u003eon boot\u003c/code\u003e, \u003ccode\u003eon late-init\u003c/code\u003e 等）\u003c/td\u003e\n          \u003ctd\u003einit 内部发射\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e有\u003c/strong\u003e — rc 晚加载也会追溯执行\u003c/td\u003e\n          \u003ctd\u003e不依赖属性的初始化\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eproperty trigger（\u003ccode\u003eon property:xxx=y\u003c/code\u003e）\u003c/td\u003e\n          \u003ctd\u003e属性值变化\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e无\u003c/strong\u003e — 错过就永远错过\u003c/td\u003e\n          \u003ctd\u003erc 必须比属性变化早加载\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"12-rc-文件的加载路径\"\u003e1.2 RC 文件的加载路径\u003c/h3\u003e\n\u003cp\u003eInit 自动 \u003ccode\u003eimport\u003c/code\u003e 以下目录的 \u003ccode\u003e.rc\u003c/code\u003e 文件：\u003c/p\u003e","title":"Android Init 开机脚本编写指南"},{"content":"Android STB 长按 Power 键弹出关机对话框 — 定位过程总结 背景 在 Android STB（Amlogic S905X5 平台）上，长按遥控器 Power 键会弹出一个系统级关机确认对话框，提示文字为 \u0026ldquo;Do you want to shut down?\u0026quot;。本文记录如何从现象出发，逐步定位到该对话框的源码位置和触发逻辑。\n第一步：通过 dumpsys 获取窗口信息 在 adb shell 中执行：\n# 1. 查看当前焦点窗口 dumpsys window | grep mCurrentFocus # 输出: mCurrentFocus=Window{8fd2b64 u0 android} # 2. 根据窗口 ID 查看详细信息 dumpsys window windows | grep -A 40 -B 10 \u0026#34;8fd2b64\u0026#34; 关键输出分析 属性 值 含义 package=android 属于 android 包 这是 Framework 系统级窗口，非第三方 App mOwnerUid=1000 system 进程 进一步确认是系统级 type=KEYGUARD_DIALOG 2009 锁屏级对话框，在所有用户上可见 fl=DIM_BEHIND ALT_FOCUSABLE_IM ... 半透明背景 + 可获取焦点 典型的模态对话框特征 Window{8fd2b64 u0 android} u0 = user 0 系统用户 结论：对话框是 Android Framework 中的系统级代码弹出的，不是任何 App 的行为。\n第二步：搜索对话框提示文字 既然确认是 Framework 代码，第一步搜索提示文字定位字符串资源：\ngrep -rn \u0026#34;Do you want to shut down\u0026#34; frameworks/base/core/res/res/values/ 找到：\n文件：frameworks/base/core/res/res/values/strings.xml\n第 630 行：\n\u0026lt;string name=\u0026#34;shutdown_confirm_question\u0026#34;\u0026gt;Do you want to shut down?\u0026lt;/string\u0026gt; 得到字符串资源 ID：com.android.internal.R.string.shutdown_confirm_question。\n第三步：搜索谁引用了这个字符串 grep -rn \u0026#34;shutdown_confirm_question\u0026#34; frameworks/base/services/ --include=\u0026#34;*.java\u0026#34; 找到唯一引用：\n文件：frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java\n第 182-184 行：\nfinal int longPressBehavior = context.getResources().getInteger( com.android.internal.R.integer.config_longPressOnPowerBehavior); final int resourceId = mRebootSafeMode ? com.android.internal.R.string.reboot_safemode_confirm : (longPressBehavior == 2 ? com.android.internal.R.string.shutdown_confirm_question : com.android.internal.R.string.shutdown_confirm); 关键发现 只有当 config_longPressOnPowerBehavior == 2（即 LONG_PRESS_POWER_SHUT_OFF）时，才会使用 \u0026ldquo;Do you want to shut down?\u0026rdquo; 这个字符串 如果是其他 behavior 值，则使用其他文字（如 \u0026ldquo;Your phone will shut down.\u0026quot;） 第 209 行设置了窗口类型：sConfirmDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 这正好吻合了第一步 dumpsys 中看到的 type=KEYGUARD_DIALOG 第四步：追踪调用 ShutdownThread 的上游 搜索谁调用了 ShutdownThread.shutdown()：\ngrep -rn \u0026#34;ShutdownThread.shutdown\\|mWindowManagerFuncs.shutdown\u0026#34; frameworks/base/services/ 找到关键调用在 PhoneWindowManager.java 的 powerLongPress() 方法中：\n文件：frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java 第 1320-1365 行 — powerLongPress() 方法： private void powerLongPress(long eventTime) { final int behavior = getResolvedLongPressOnPowerBehavior(); switch (behavior) { case LONG_PRESS_POWER_NOTHING: break; case LONG_PRESS_POWER_GLOBAL_ACTIONS: mPowerKeyHandled = true; showGlobalActions(); // 显示全局操作菜单 break; case LONG_PRESS_POWER_SHUT_OFF: case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM: mPowerKeyHandled = true; mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF); // confirm=true → 弹出确认对话框 // confirm=false → 直接关机 break; case LONG_PRESS_POWER_GO_TO_VOICE_ASSIST: // 启动语音助手 break; case LONG_PRESS_POWER_ASSISTANT: // 启动助理 break; } } 第五步：追踪电源键手势识别 长按手势是如何被检测到的？\n在 PhoneWindowManager.java 中有一个内部类 PowerKeyRule（第 2516-2573 行），它继承自 SingleKeyGestureDetector.SingleKeyRule：\nprivate final class PowerKeyRule extends SingleKeyGestureDetector.SingleKeyRule { PowerKeyRule() { super(KEYCODE_POWER); } boolean supportLongPress() { return hasLongPressOnPowerBehavior(); } void onLongPress(long eventTime) { if (beganFromNonInteractive \u0026amp;\u0026amp; !mSupportLongPressPowerWhenNonInteractive) return; powerLongPress(eventTime); // → 触发上述 switch } } 手势检测的触发链：\ninterceptKeyBeforeQueueing() // 第 4309 行 → KEYCODE_POWER case (第 4584 行) → interceptPowerKeyDown() // 处理 wake lock 等 → handleKeyGesture() // 第 4805 行 → SingleKeyGestureDetector.interceptKey() → PowerKeyRule.onLongPress() → powerLongPress() 第六步：确认实际生效的配置值 AOSP 框架默认值在：\nframeworks/base/core/res/res/values/config.xml 第 1100 行：config_longPressOnPowerBehavior = 5（助理） 但实际设备上的值被 Amlogic 的 DroidOverlay 覆盖了：\nvendor/amlogic/common/apps/VendorOverlay/DroidOverlay/res/values/config.xml 第 188 行：\n\u0026lt;integer name=\u0026#34;config_longPressOnPowerBehavior\u0026#34;\u0026gt;2\u0026lt;/integer\u0026gt; 这确认了当前设备上 behavior = 2（LONG_PRESS_POWER_SHUT_OFF），即长按 Power 键 = 弹出确认对话框后关机。\nconfig_longPressOnPowerBehavior 可选值 值 常量 行为 0 LONG_PRESS_POWER_NOTHING 无操作 1 LONG_PRESS_POWER_GLOBAL_ACTIONS 显示全局操作菜单 2 LONG_PRESS_POWER_SHUT_OFF 关机（带确认对话框）← 当前配置 3 LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM 关机（无确认，直接关） 4 LONG_PRESS_POWER_GO_TO_VOICE_ASSIST 语音助手 5 LONG_PRESS_POWER_ASSISTANT 助理 ← AOSP 默认 完整调用链路图 ┌──────────────────────────────────────────────────────────────────────┐ │ 遥控器 Power 键（长按） │ └──────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ Linux Kernel Input 子系统 → /dev/input/eventX │ └──────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ InputReader (frameworks/native/services/inputflinger) │ │ 读取原始输入事件，分发给 InputDispatcher │ └──────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ PhoneWindowManager.interceptKeyBeforeQueueing() │ │ frameworks/base/services/core/java/com/android/server/policy/ │ │ PhoneWindowManager.java │ │ ├── KEYCODE_POWER → interceptPowerKeyDown() │ │ └── handleKeyGesture() → SingleKeyGestureDetector │ └──────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ PowerKeyRule.onLongPress() │ │ → powerLongPress(eventTime) 第 1320 行 │ │ → getResolvedLongPressOnPowerBehavior() │ │ → behavior = 2 (LONG_PRESS_POWER_SHUT_OFF) │ │ （由 vendor/amlogic/.../DroidOverlay/config.xml 覆盖） │ │ → mWindowManagerFuncs.shutdown(true) 第 1345 行 │ └──────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ ShutdownThread.shutdownInner(context, confirm=true) │ │ frameworks/base/services/core/java/com/android/server/power/ │ │ ShutdownThread.java 第 158 行 │ │ ├── 读取 config_longPressOnPowerBehavior = 2 │ │ ├── 因为 behavior == 2，选用 shutdown_confirm_question 字符串 │ │ │ 即 \u0026#34;Do you want to shut down?\u0026#34; 第 182 行 │ │ ├── 构建 AlertDialog 第 195 行 │ │ │ ├── Title: \u0026#34;Power off\u0026#34; │ │ │ ├── Message: \u0026#34;Do you want to shut down?\u0026#34; │ │ │ ├── Positive: \u0026#34;Yes\u0026#34; → beginShutdownSequence() │ │ │ └── Negative: \u0026#34;No\u0026#34; │ │ └── 设置窗口类型为 TYPE_KEYGUARD_DIALOG 第 209 行 │ │ → dialog.show() │ └──────────────────────────────────────────────────────────────────────┘ 涉及的关键文件一览 文件路径 作用 frameworks/base/core/res/res/values/strings.xml:630 定义 \u0026ldquo;Do you want to shut down?\u0026rdquo; 字符串 frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java:158-213 创建并显示关机确认对话框 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java:1320-1365 powerLongPress() — 根据 behavior 决定长按电源键的行为 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java:2516-2573 PowerKeyRule — 电源键手势识别规则 frameworks/base/core/java/android/view/WindowManager.java:2098 TYPE_KEYGUARD_DIALOG 常量定义 frameworks/base/core/res/res/values/config.xml:1100 AOSP 默认 config_longPressOnPowerBehavior = 5 vendor/amlogic/common/apps/VendorOverlay/DroidOverlay/res/values/config.xml:188 实际生效的覆盖配置：config_longPressOnPowerBehavior = 2 调试方法论总结 定位这类系统级 UI 问题的通用方法：\n先确认窗口归属：dumpsys window 查看 package 和窗口 type，判断是 System 还是 App 的窗口 搜索字符串：根据 UI 上显示的文字在 strings.xml 中找到资源 ID 反向追踪引用：搜索谁引用了这个资源 ID，找到 UI 创建代码 正向追踪调用链：从 UI 创建处向上游搜索调用者，找到触发入口 确认实际配置：搜索所有 overlay（包括 device/、vendor/ 目录），找到最终生效的配置值，因为 AOSP 默认值往往会被设备厂商覆盖 实战案例：将长按 Power 键从关机改为重启 需求 将长按遥控器 Power 键的行为从\u0026quot;弹出关机确认对话框并关机\u0026quot;改为\u0026quot;弹出重启确认对话框并重启\u0026rdquo;。\n需求分析 回顾当前的调用链路：\nPhoneWindowManager.powerLongPress() → behavior = 2 (LONG_PRESS_POWER_SHUT_OFF) → mWindowManagerFuncs.shutdown(true) → WindowManagerService.shutdown(true) → ShutdownThread.shutdown(context, reason, true) → mReboot = false → shutdownInner(context, true) → 构建 AlertDialog ├── Title: \u0026#34;Power off\u0026#34; ├── Message: \u0026#34;Do you want to shut down?\u0026#34; ├── Positive: \u0026#34;Yes\u0026#34; → beginShutdownSequence() → 关机 └── Negative: \u0026#34;No\u0026#34; 关键发现：WindowManagerFuncs 接口已经内置了 reboot(boolean confirm) 方法：\n// WindowManagerPolicy.java:254-256 public void shutdown(boolean confirm); public void reboot(boolean confirm); public void rebootSafeMode(boolean confirm); 而 ShutdownThread 也已有 reboot() 静态方法（第 247-253 行），它设置 mReboot = true 后调用同一个 shutdownInner()，底层 run() 方法会根据 mReboot 标志决定执行 lowLevelReboot() 还是 lowLevelShutdown()。\n但问题在于：shutdownInner() 构建对话框时没有检查 mReboot 标志，所以直接调用 reboot() 会导致对话框仍然显示 \u0026ldquo;Power off\u0026rdquo; / \u0026ldquo;Do you want to shut down?\u0026quot;，但点击确认后实际执行的是重启——这是不一致的用户体验。\n因此需要修改三处：\nPhoneWindowManager：将 shutdown() 调用改为 reboot() ShutdownThread：在 shutdownInner() 中根据 mReboot 显示不同的对话框文字 strings.xml：新增重启确认的字符串资源 方案对比 在实施前考虑了三种方案：\n方案 改动量 用户体验 是否采用 方案一：改 PhoneWindowManager + ShutdownThread + 字符串 3 个文件 直接弹出重启确认框 ✅ 采用 方案二：改 overlay 配置，behavior 从 2 改为 1 1 个文件 弹出全局操作菜单，需再选重启 ❌ 体验不佳 方案三：利用已有 persist.sys.power.key.action 属性 需额外开发 运行时配置 ❌ 仅支持短按 选择方案一的理由：\nWindowManagerFuncs.reboot() 已经实现，底层基础设施完备 直接弹出 \u0026ldquo;Do you want to restart?\u0026rdquo; 确认框，用户体验最佳 改动精准，不影响其他调用路径（如 ShutdownThread.shutdown() 的调用者） 同时完善了 shutdownInner() 的逻辑，使所有通过 reboot() 的调用都能显示正确的对话框 具体修改 改动 1：PhoneWindowManager.java（长按行为从 shutdown 改为 reboot） 文件：frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java 位置：powerLongPress() 方法，第 1342-1345 行\nperformHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false, - \u0026#34;Power - Long Press - Shut Off\u0026#34;); + \u0026#34;Power - Long Press - Reboot\u0026#34;); sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); -mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF); +mWindowManagerFuncs.reboot(behavior == LONG_PRESS_POWER_SHUT_OFF); 说明：\nbehavior == LONG_PRESS_POWER_SHUT_OFF 仍然为 true（因为 overlay 配置 config_longPressOnPowerBehavior = 2），所以 confirm 参数为 true，会弹出确认对话框 调用链变为：mWindowManagerFuncs.reboot(true) → WindowManagerService.reboot(true) → ShutdownThread.reboot(context, reason, true) → mReboot = true → shutdownInner(context, true) 改动 2：ShutdownThread.java（对话框文字根据 mReboot 显示） 文件：frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java 位置：shutdownInner() 方法\n改动 2a：消息文字（第 180-185 行）\nfinal int resourceId = mRebootSafeMode ? com.android.internal.R.string.reboot_safemode_confirm - : (longPressBehavior == 2 - ? com.android.internal.R.string.shutdown_confirm_question - : com.android.internal.R.string.shutdown_confirm); + : (mReboot + ? com.android.internal.R.string.reboot_confirm_question + : (longPressBehavior == 2 + ? com.android.internal.R.string.shutdown_confirm_question + : com.android.internal.R.string.shutdown_confirm)); 改动 2b：标题文字（第 196-199 行）\n.setTitle(mRebootSafeMode ? com.android.internal.R.string.reboot_safemode_title - : com.android.internal.R.string.power_off) + : (mReboot + ? com.android.internal.R.string.global_action_restart + : com.android.internal.R.string.power_off)) 说明：\n优先级：mRebootSafeMode \u0026gt; mReboot \u0026gt; longPressBehavior mReboot=true 时，使用 global_action_restart（\u0026ldquo;Restart\u0026rdquo;）作为标题，reboot_confirm_question（\u0026ldquo;Do you want to restart?\u0026quot;）作为消息 不影响 mReboot=false 的调用路径（如直接调用 ShutdownThread.shutdown() 的场景） 改动 3：strings.xml（新增字符串资源） 文件：frameworks/base/core/res/res/values/strings.xml 位置：shutdown_confirm_question 之后\n\u0026lt;!-- Reboot Confirmation Dialog. When the user chooses to restart the device, it asks the user if they\u0026#39;d like to reboot. This is the message. This is used instead of shutdown_confirm when the system is configured to use long press to go directly to the reboot dialog instead of shutdown. --\u0026gt; \u0026lt;string name=\u0026#34;reboot_confirm_question\u0026#34;\u0026gt;Do you want to restart?\u0026lt;/string\u0026gt; 修改后的调用链路 PhoneWindowManager.powerLongPress() → behavior = 2 (LONG_PRESS_POWER_SHUT_OFF) → mWindowManagerFuncs.reboot(true) ← 改为 reboot → WindowManagerService.reboot(true) → ShutdownThread.reboot(context, reason, true) → mReboot = true ← 设置重启标志 → shutdownInner(context, true) → 读取 config_longPressOnPowerBehavior = 2 → mReboot = true, 选用 reboot_confirm_question → 构建 AlertDialog ├── Title: \u0026#34;Restart\u0026#34; ← 改为 Restart ├── Message: \u0026#34;Do you want to restart?\u0026#34; ← 改为重启文字 ├── Positive: \u0026#34;Yes\u0026#34; → beginShutdownSequence() → 重启 └── Negative: \u0026#34;No\u0026#34; 对话框文字对照 调用方式 mReboot 标题 消息 确认后行为 shutdown(..., true) (直接关机) false Power off Do you want to shut down? 关机 shutdown(..., true) (behavior=2) false Power off Do you want to shut down? 关机 reboot(..., true) (长按 Power 改后) true Restart Do you want to restart? 重启 rebootSafeMode(..., true) true + safeMode Reboot to safe mode Do you want to reboot into safe mode? 安全模式重启 扩展思考：如何将长按 Power 改为其他功能？ 基于这个案例的分析，如果要将长按 Power 改为启动自定义 Activity、发送广播、或其他功能，有以下几种方式：\n方式 A：修改 config_longPressOnPowerBehavior overlay \u0026lt;!-- vendor/amlogic/common/apps/VendorOverlay/DroidOverlay/res/values/config.xml --\u0026gt; \u0026lt;!-- 改为 1：全局操作菜单；改为 0：无操作 --\u0026gt; \u0026lt;integer name=\u0026#34;config_longPressOnPowerBehavior\u0026#34;\u0026gt;1\u0026lt;/integer\u0026gt; 方式 B：在 powerLongPress() 中新增 case 在 PhoneWindowManager.java 的 powerLongPress() 中增加自定义行为分支：\ncase CUSTOM_ACTION: mPowerKeyHandled = true; // 启动自定义 Activity Intent intent = new Intent(\u0026#34;com.example.CUSTOM_ACTION\u0026#34;); mContext.startActivity(intent); break; 方式 C：利用 Amlogic 的 persist.sys.power.key.action 属性 # 短按关机 (当前 powerPress() 已支持) setprop persist.sys.power.key.action 1 # 短按重启 setprop persist.sys.power.key.action 2 此属性的处理逻辑在 PhoneWindowManager.powerPress() 第 1057 行，仅影响短按。如需影响长按，可在 powerLongPress() 中增加类似判断逻辑。\n本案例关键经验 Framework 接口往往已经提供了所需能力——WindowManagerFuncs.reboot() 和 ShutdownThread.reboot() 都已存在，只需要把它们串起来 修改系统对话框文字时，要检查底层方法的\u0026quot;隐式假设\u0026rdquo;——shutdownInner() 方法名暗示它只为关机服务，但实际上 reboot() 也复用它，只是它内部没有正确处理 mReboot 标志 静态变量的状态传递——mReboot 是静态变量，在 shutdown()/reboot() 中设置，在 shutdownInner() 和 run() 中使用。这种隐式的状态传递是遗留代码常见模式，修改时需要注意不要破坏它 字符串资源要放在正确的 values 目录——框架代码引用 com.android.internal.R.string.*，因此字符串必须定义在 frameworks/base/core/res/res/values/strings.xml 中，不能放在 vendor overlay 里 ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/%E4%BF%AE%E6%94%B9%E9%81%A5%E6%8E%A7%E5%99%A8power%E6%8C%89%E9%94%AE%E7%9A%84%E9%80%BB%E8%BE%91/","summary":"\u003ch1 id=\"android-stb-长按-power-键弹出关机对话框--定位过程总结\"\u003eAndroid STB 长按 Power 键弹出关机对话框 — 定位过程总结\u003c/h1\u003e\n\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e在 Android STB（Amlogic S905X5 平台）上，长按遥控器 Power 键会弹出一个系统级关机确认对话框，提示文字为 \u0026ldquo;Do you want to shut down?\u0026quot;。本文记录如何从现象出发，逐步定位到该对话框的源码位置和触发逻辑。\u003c/p\u003e\n\u003ch2 id=\"第一步通过-dumpsys-获取窗口信息\"\u003e第一步：通过 dumpsys 获取窗口信息\u003c/h2\u003e\n\u003cp\u003e在 \u003ccode\u003eadb shell\u003c/code\u003e 中执行：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 1. 查看当前焦点窗口\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edumpsys window | grep mCurrentFocus\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 输出: mCurrentFocus=Window{8fd2b64 u0 android}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 2. 根据窗口 ID 查看详细信息\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edumpsys window windows | grep -A \u003cspan style=\"color:#ae81ff\"\u003e40\u003c/span\u003e -B \u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;8fd2b64\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"关键输出分析\"\u003e关键输出分析\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e属性\u003c/th\u003e\n          \u003cth\u003e值\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003epackage=android\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e属于 \u003ccode\u003eandroid\u003c/code\u003e 包\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e这是 Framework 系统级窗口，非第三方 App\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003emOwnerUid=1000\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003esystem 进程\u003c/td\u003e\n          \u003ctd\u003e进一步确认是系统级\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003etype=KEYGUARD_DIALOG\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e2009\u003c/td\u003e\n          \u003ctd\u003e锁屏级对话框，在所有用户上可见\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003efl=DIM_BEHIND ALT_FOCUSABLE_IM ...\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e半透明背景 + 可获取焦点\u003c/td\u003e\n          \u003ctd\u003e典型的模态对话框特征\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eWindow{8fd2b64 u0 android}\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eu0 = user 0\u003c/td\u003e\n          \u003ctd\u003e系统用户\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e结论\u003c/strong\u003e：对话框是 Android Framework 中的系统级代码弹出的，不是任何 App 的行为。\u003c/p\u003e","title":"Android STB 长按 Power 键弹出关机对话框 — 定位过程总结"},{"content":"RTC 唤醒注入 KEY_POWER — 代码路径分析 背景 rtk-parker-aosp-squash-25q2.02-mirakulo项目\n概述 STB 待机时 RTC 定时唤醒，IR 驱动在 Resume 时从寄存器读到上一个按键（POWER）的 NEC 帧残留，因 KEY_E 不在键值表中导致查表失败，旧代码 fallback 注入 KEY_POWER，经 Android → CEC 触发电视开机。\n根因是两层问题叠加： ① 键值表缺少 KEY_E 映射；② 旧代码在 KEY_E 查不到时无条件使用寄存器残留值调用 rc_keydown()。\n一、待机前：POWER 键的 NEC 帧锁存在寄存器中 用户按遥控器 POWER 让 STB 待机。IR 硬件收到 NEC 帧后存入 ISO 寄存器 0x65c。\n正常 ISR 路径（rtk-ir-hw.c:100-139）读出并解码，发送按键。Suspend 时 ISR 已不再触发，但硬件寄存器继续锁存上次收到的数据。\nrtk-ir-hw.c:76-86 的 suspend 路径只 drain FIFO，不清零数据寄存器，残留值保留。\n二、RTC 唤醒：PM notifier 读取寄存器 RTC 到期 → SoC 唤醒 → PM notifier 回调 rtk_ir_key_event()。\nrtk-ir-core.c:108-112：\nregmap_read(ir-\u0026gt;iso, 0x65c, \u0026amp;val); // 读 RX FIFO → 残留值 0xf50a4987 regmap_read(ir-\u0026gt;iso, 0x650, \u0026amp;nf_code); // 噪声检测 → 0（无真实 IR 信号） pr_err(\u0026#34;Wakeup entire android src %d, 0x%x, %d\\n\u0026#34;, wakeup_event, val, nf_code); // 输出: Wakeup entire android src 3, 0xf50a4987, 0 dec-\u0026gt;scancode(\u0026amp;request, val); // 解码 唤醒事件类型定义在 rtk_pm_pcpu.h:25-31：\nenum rtk_wakeup_event { LAN_EVENT = 0, IR_EVENT, // 1 — 遥控器按键唤醒 GPIO_EVENT, // 2 — GPIO 唤醒 ALARM_EVENT, // 3 — RTC 定时器唤醒 ← 本次 // ... }; 三、NEC 解码：0xf50a4987 → 0x87490a rtk-ir-nec.c:11-37：\nstatic int rtk_ir_nec_scancode(struct rtk_ir_scancode_req *request, unsigned int raw) { addr = (raw \u0026gt;\u0026gt; 0) \u0026amp; 0xff; // 0x87 addr_inv = (raw \u0026gt;\u0026gt; 8) \u0026amp; 0xff; // 0x49 data = (raw \u0026gt;\u0026gt; 16) \u0026amp; 0xff; // 0x0a data_inv = (raw \u0026gt;\u0026gt; 24) \u0026amp; 0xff; // 0xf5 // (data_inv ^ data) = 0xff → 不走 NEC32 // (addr_inv ^ addr) = 0xCE ≠ 0xff → 走 NECX request-\u0026gt;scancode = addr \u0026lt;\u0026lt; 16 | addr_inv \u0026lt;\u0026lt; 8 | data; = 0x87 \u0026lt;\u0026lt; 16 | 0x49 \u0026lt;\u0026lt; 8 | 0x0a; = 0x87490a; // ← 恰是 KEY_POWER 的 scancode request-\u0026gt;protocol = RC_PROTO_NECX; } 解码出的 0x87490a 就是用户在待机前按下 POWER 键的 scancode。\n四、查找 KEY_E 失败 — scancode 未被覆盖 rtk-ir-core.c:114-141 根据唤醒类型决定使用什么 scancode：\nif (wakeup_event == IR_EVENT) { // 在 map 中匹配当前 scancode } else if (wakeup_event == GPIO_EVENT) { // 查 KEY_PROG1（Netflix 按钮） } else if (wakeup_event == ALARM_EVENT) { // ★ 走这里 — 查找 KEY_E for (k = 0; k \u0026lt; map-\u0026gt;len; k++) if (KEY_E == (map-\u0026gt;scan + k)-\u0026gt;keycode) { request.scancode = (map-\u0026gt;scan + k)-\u0026gt;scancode; // 用 KEY_E 的 scancode 覆盖 break; } // GIEC 的 rc-realtek-dhc 中没有 KEY_E → k \u0026gt;= map-\u0026gt;len // request.scancode 未被覆盖 → 仍是解码出来的 0x87490a } else { // 其他事件 — 查 KEY_POWER } GIEC 的键值表 rc-realtek-dhc.c 中没有 KEY_E 条目：\nstatic struct rc_map_table realtek_dhc[] = { // ... { 0x87490A, KEY_POWER }, // ← KEY_POWER 在表中 { 0x8869, KEY_Y }, { 0x8870, KEY_U }, // ... 没有 KEY_E }; 五、旧代码无条件注入 → 命中 KEY_POWER 修复前的 rtk-ir-core.c（注入阶段）：\n} else if (wakeup_event == ALARM_EVENT) { pr_err(\u0026#34;[ALARM_RESUME][%s] Check Wi-Fi connection, scan code: 0x%x\\n\u0026#34;, __func__, request.scancode); // 输出: [ALARM_RESUME][rtk_ir_key_event] Check Wi-Fi connection, scan code: 0x87490a rc_keydown(rc, map-\u0026gt;rc_proto, request.scancode, 0); // 无条件注入 // → rc_keydown(NECX, 0x87490a, 0) } rc-main.c:848-855 的 rc_keydown()：\nvoid rc_keydown(struct rc_dev *dev, enum rc_proto protocol, u64 scancode, u8 toggle) { u32 keycode = rc_g_keycode_from_table(dev, scancode); // 查表 ir_do_keydown(dev, protocol, scancode, keycode, toggle); } rc_g_keycode_from_table()（rc-main.c:605-625）对 0x87490a 二分查找 → 命中 rc-realtek-dhc 中的 { 0x87490A, KEY_POWER } → 返回 KEY_POWER (116) → Android 收到电源键事件。\n六、Android 到 CEC 到电视开机 input_report_key(KEY_POWER, 1) → /vendor/usr/keylayout/rtk-ir-hw.kl → key 116 POWER WAKE → PhoneWindowManager → 检测系统从待机恢复 → 触发 HDMI-CEC → HdmiControlService → \u0026lt;Active Source\u0026gt; + \u0026lt;One Touch Play\u0026gt; → 电视收到 CEC → 开机 七、完整链路 待机前用户按 POWER → IR 硬件捕获 NEC 帧 → ISO 0x65c = 0xf50a4987 → STB 进入 Suspend（寄存器不清零） │ RTC 到期 → SoC 唤醒 → PM notifier: rtk_ir_key_event() → regmap_read(0x65c) → val = 0xf50a4987（残留值） → NECX 解码 → scancode = 0x87490a → ALARM_EVENT → 查 map 找 KEY_E → 找不到 → request.scancode 未被覆盖 → 仍是 0x87490a → 旧代码无条件 rc_keydown(0x87490a) → rc_g_keycode_from_table() → 命中 KEY_POWER → input_report_key(KEY_POWER, 1) → Android → CEC One Touch Play → TV ON 八、修复 ps: 最终采用了直接将\nswitch (wakeup_event) { case ALARM_EVENT: sendKeyEvent = false; ALARM_EVENT的sendKeyEvent置为false，直接不注入按键到上层\n8.1 解析层 — KEY_E 找不到时 scancode 置 0 rtk-ir-core.c:127-133\n} else if (wakeup_event == ALARM_EVENT) { for (k = 0; k \u0026lt; map-\u0026gt;len; k++) if (KEY_E == (map-\u0026gt;scan + k)-\u0026gt;keycode) { request.scancode = (map-\u0026gt;scan + k)-\u0026gt;scancode; break; } if (k == map-\u0026gt;len) request.scancode = 0; // KEY_E 找不到 → 显式清零 } scancode = 0 永远不会命中键值表中的任何条目，确保 rc_keydown() 即使被调用也不会注入有效按键。\n8.2 注入层 — KEY_E 找不到时跳过注入 rtk-ir-core.c:152-160\n} else if (wakeup_event == ALARM_EVENT) { if (k \u0026lt; map-\u0026gt;len) { pr_err(\u0026#34;[ALARM_RESUME]... scan code: 0x%x\\n\u0026#34;, __func__, request.scancode); rc_keydown(rc, map-\u0026gt;rc_proto, request.scancode, 0); } else { pr_err(\u0026#34;KEY_E not in rc_map, skip ALARM inject\\n\u0026#34;); // 找不到 → 跳过 } } 8.3 键值表 — 添加 KEY_E 映射（可选） rc-realtek-dhc.c：\n{ 0x8749FF, KEY_E }, // KEY_E for wifi connected 添加后，RTC 唤醒时 KEY_E 被注入到 Android → GlobalKeyReceiver 收到 BUTTON_8 → 后台 WiFi 重连 → 释放 WakeLock → 系统睡回。\n涉及文件 文件 改动 drivers/media/rc/rtk-ir/rtk-ir-core.c 解析层 scancode 置 0 + 注入层 skip drivers/media/rc/rtk-ir/rtk-ir-nec.c NEC 解码器（未改，链路中说明） drivers/media/rc/keymaps/rc-realtek-dhc.c 添加 KEY_E 映射（可选） drivers/media/rc/rc-main.c rc_keydown + 查表逻辑（未改，链路中说明） include/soc/realtek/uapi/rtk_pm_pcpu.h 唤醒事件枚举 ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/rtc%E5%94%A4%E9%86%92%E5%BC%95%E8%B5%B7%E7%9A%84%E5%BE%85%E6%9C%BA%E9%97%AE%E9%A2%98/0.%E7%B3%BB%E7%BB%9F%E5%BE%85%E6%9C%BA%E7%8A%B6%E6%80%81%E8%A2%AB%E5%BC%82%E5%B8%B8%E5%94%A4%E9%86%92/","summary":"\u003ch1 id=\"rtc-唤醒注入-key_power--代码路径分析\"\u003eRTC 唤醒注入 KEY_POWER — 代码路径分析\u003c/h1\u003e\n\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003ertk-parker-aosp-squash-25q2.02-mirakulo项目\u003c/p\u003e\n\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003eSTB 待机时 RTC 定时唤醒，IR 驱动在 Resume 时从寄存器读到上一个按键（POWER）的 NEC 帧残留，因 KEY_E 不在键值表中导致查表失败，旧代码 fallback 注入 KEY_POWER，经 Android → CEC 触发电视开机。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e根因是两层问题叠加：\u003c/strong\u003e ① 键值表缺少 KEY_E 映射；② 旧代码在 KEY_E 查不到时无条件使用寄存器残留值调用 \u003ccode\u003erc_keydown()\u003c/code\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一待机前power-键的-nec-帧锁存在寄存器中\"\u003e一、待机前：POWER 键的 NEC 帧锁存在寄存器中\u003c/h2\u003e\n\u003cp\u003e用户按遥控器 POWER 让 STB 待机。IR 硬件收到 NEC 帧后存入 ISO 寄存器 \u003ccode\u003e0x65c\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e正常 ISR 路径（\u003ccode\u003ertk-ir-hw.c:100-139\u003c/code\u003e）读出并解码，发送按键。Suspend 时 ISR 已不再触发，但硬件寄存器继续锁存上次收到的数据。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ertk-ir-hw.c:76-86\u003c/code\u003e 的 suspend 路径只 drain FIFO，不清零数据寄存器，残留值保留。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"二rtc-唤醒pm-notifier-读取寄存器\"\u003e二、RTC 唤醒：PM notifier 读取寄存器\u003c/h2\u003e\n\u003cp\u003eRTC 到期 → SoC 唤醒 → PM notifier 回调 \u003ccode\u003ertk_ir_key_event()\u003c/code\u003e。\u003c/p\u003e","title":"0.系统待机状态被异常唤醒"},{"content":"HDMI 被 RTC 唤醒 — 代码路径分析 背景 rtk-parker-aosp-squash-25q2.02-mirakulo项目\n前置问题见：系统待机状态被异常唤醒.md\n概述 RTC 定时唤醒 STB 时，HDMI 会短暂输出信号导致电视黑屏，然后恢复无信号。\n根因：内核 PM 框架的 Resume 不判断唤醒源，无条件恢复所有设备，HDMI 驱动在 resume 时无条件打开 PHY。\n修复方向：在 rtk_drm_resume() 和 rtk_hdmi_resume() 中判断唤醒源，RTC 后台唤醒时跳过显示/PHY 初始化。\n一、系统唤醒总入口 kernel/power/suspend.c:490-529\nint suspend_devices_and_enter(suspend_state_t state) { // ① suspend 所有设备 dpm_suspend_start(PMSG_SUSPEND); // ② SoC 深度睡眠 suspend_enter(state, \u0026amp;wakeup); Resume_devices: // ③ 被 RTC/IR/GPIO 唤醒后从这里开始 dpm_resume_end(PMSG_RESUME); // 恢复所有设备 } 醒来后 dpm_resume_end() 是唯一出口，不管谁唤醒的都走这里。\n二、dpm_resume — 无差别恢复所有设备 drivers/base/power/main.c:1001-1040\nvoid dpm_resume(pm_message_t state) { while (!list_empty(\u0026amp;dpm_suspended_list)) { // 遍历所有已 suspend 的设备 dev = to_device(dpm_suspended_list.next); device_resume(dev); // 逐个恢复 } } dpm_suspended_list 是内核 PM 框架维护的全局链表，所有被 suspend 的设备都在上面。HDMI 设备在 suspend 时被加入链表，resume 时必然被取出恢复。\n这个 while 循环没有任何 if (wakeup_source == RTC) skip 的判断。\n三、device_resume 如何找到 HDMI 的回调 drivers/base/power/main.c:888-958\nstatic void __device_resume(struct device *dev, pm_message_t state) { // 按优先级找回调：pm_domain → type → class → bus → driver if (!callback \u0026amp;\u0026amp; dev-\u0026gt;driver \u0026amp;\u0026amp; dev-\u0026gt;driver-\u0026gt;pm) callback = pm_op(dev-\u0026gt;driver-\u0026gt;pm, state); // PMSG_RESUME → .resume 回调 dpm_run_callback(callback, dev, state, info); // 调用回调 } pm_op() 根据 state.event == PM_EVENT_RESUME 返回 ops-\u0026gt;resume。\nHDMI 驱动注册了 dev_pm_ops，rtk_hdmi.c:2383-2402：\nstatic const struct dev_pm_ops rtk_hdmi_pm_ops = { .resume = rtk_hdmi_resume, // ← 被 pm_op() 返回 }; struct platform_driver rtk_hdmi_driver = { .driver = { .name = \u0026#34;rtk-hdmi\u0026#34;, .pm = \u0026amp;rtk_hdmi_pm_ops, // ← dev-\u0026gt;driver-\u0026gt;pm 指向这里 }, }; DRM 驱动也注册了自己的 resume：\nrtk_drm_drv.c:244-256：\nstatic int rtk_drm_resume(struct device *dev) { return drm_mode_config_helper_resume(drm); // 恢复 CRTC/Plane/Encoder } 四、rtk_hdmi_resume — 无条件打开 PHY rtk_hdmi.c:2294-2352\nstatic int rtk_hdmi_resume(struct device *dev) { hdmi-\u0026gt;in_suspend = false; gpiod_set_debounce(hdmi-\u0026gt;hpd_gpio, 30*1000); // HDCP 重新协商 ... hdmi-\u0026gt;hdmi_ops-\u0026gt;update_hpd_state(hdmi); // ← 关键调用 enable_irq(hdmi-\u0026gt;hpd_irq); return 0; } update_hpd_state() 是主动调用，不是硬件自动行为。 这里不判断唤醒源，任何 resume 都会触发后续的热插拔链路。\n五、update_hpd_state — 判定为\u0026quot;新连接\u0026quot; rtk_hdmi.c:1342-1414\nstatic bool rtk_hdmi_update_hpd_state(struct rtk_hdmi *hdmi) { hpd = gpiod_get_value(hdmi-\u0026gt;hpd_gpio); // TV 连着 → 1 if ((hdmi-\u0026gt;hpd_state == 0) \u0026amp;\u0026amp; (hpd == 1)) { // suspend 后 hpd_state 被清零 hdmi-\u0026gt;edid_cache = drm_get_edid(\u0026amp;hdmi-\u0026gt;connector, hdmi-\u0026gt;ddc); // 读 EDID } hdmi-\u0026gt;hpd_state = hpd; is_connected = hpd \u0026amp;\u0026amp; rxsense; if (is_connected != pre_state) { drm_helper_hpd_irq_event(hdmi-\u0026gt;connector.dev); // ★ 触发 DRM 热插拔 extcon_set_state_sync(hdmi-\u0026gt;edev, EXTCON_DISP_HDMI, true); } } Suspend 时 PHY 断电 → hpd_state 复位为 0。Resume 后读 GPIO 得到 1 → (hpd_state == 0) \u0026amp;\u0026amp; (hpd == 1) 成立 → 当作\u0026quot;新连接\u0026quot;处理：读 EDID、通知 DRM。\n六、drm_helper_hpd_irq_event — 触发 modeset drivers/gpu/drm/drm_probe_helper.c:831-881\nbool drm_helper_hpd_irq_event(struct drm_device *dev) { drm_kms_helper_hotplug_event(dev); // → drm_client_dev_hotplug(dev) // → connector-\u0026gt;detect() → connector_status_connected // → drm_fb_helper_hotplug_event() // → drm_fb_helper_force_modeset() // fb_helper 自动设置显示模式 // → drm_atomic_commit() // → encoder-\u0026gt;enable() } drm_fb_helper 是内核自带的 framebuffer 辅助层。当检测到 connector 变为 connected，它自动配置显示模式——不需要用户空间参与。\n七、encoder enable — 真正打开 PHY rtk_hdmi.c:1692-1663\n// DRM 框架调用 encoder 的 enable 回调 static void rtk_hdmi_enc_enable(struct drm_encoder *encoder) { rtk_hdmi_setup(hdmi, \u0026amp;hdmi-\u0026gt;previous_mode); } // rtk_hdmi_setup 内部： static void rtk_hdmi_setup(struct rtk_hdmi *hdmi, struct drm_display_mode *mode) { hdmitx_set_video_timing(hdmi, mode); // 配置 video timing → PHY 上电 rtk_hdmi_send_avmute(hdmi, 0); // 取消 AVMUTE → 开始输出画面 hdmi-\u0026gt;is_hdmi_on = true; } 到这一步，TMDS 信号开始输出，电视检测到信号 → 黑屏。\n八、Suspend 时如何关闭 rtk_hdmi.c:2250-2291 + rtk_hdmi.c:1701-1708\n// PM suspend 回调 static int rtk_hdmi_suspend(struct device *dev) { disable_irq(hdmi-\u0026gt;hpd_irq); rtk_hdmi_off(hdmi, OFF_FLAG_HDCP | OFF_FLAG_DIS_AUDIO); // 关 PHY return 0; } // DRM encoder disable 回调 static void rtk_hdmi_enc_disable(struct drm_encoder *encoder) { rtk_hdmi_off(hdmi, OFF_FLAG_NORMAL); // 关 PHY } 九、Suspend 为什么会失败一次 日志中常出现 PM: Some devices failed to suspend, or early wake event detected。\ndrivers/base/power/main.c:1633-1637：\nstatic int __device_suspend(struct device *dev, ...) { if (pm_wakeup_pending()) { // 检测到未处理的唤醒事件 async_error = -EBUSY; // 不打印设备名，直接中止 goto Complete; } // ... } 原因是 RTC 唤醒后在 re-suspend 过程中 PCPU 发出了一个无效的唤醒通知（wakeup_event = 10 = MAX_EVENT），pm_wakeup_pending() 检测到后中止了第一次 suspend。恢复后重试，第二次成功。\n根因在 rtk_pm.c:157：\ndev_pm-\u0026gt;wakeup_reason = MAX_EVENT; // suspend 时初始化为 MAX_EVENT 如果 PCPU 在 re-suspend 前来不及更新正确的唤醒源，IR 驱动就会读到 10，打印 Bad wakeup event value 10。\n十、完整链路（一张图） RTC 到期 → PCPU 唤醒 SoC → suspend_devices_and_enter() → Resume_devices → dpm_resume_end(PMSG_RESUME) → dpm_resume() 遍历 dpm_suspended_list → device_resume(HDMI_DEV) → rtk_hdmi_pm_ops.resume = rtk_hdmi_resume() → update_hpd_state(hdmi) → hpd=1, hpd_state=0 → 读 EDID → drm_helper_hpd_irq_event() → drm_kms_helper_hotplug_event() → fb_helper 自动 modeset → rtk_hdmi_enc_enable() → rtk_hdmi_setup() → PHY 上电 → TMDS 输出 → 电视黑屏 这条链路上没有任何一步判断唤醒源。只要 SoC resume，HDMI 必然亮屏。\n十一、修复方案 在这条链路的两个入口处加唤醒源判断：\n11.1 DRM 层 — rtk_drm_resume rtk_drm_drv.c:251-255\nstatic int rtk_drm_resume(struct device *dev) { struct drm_device *drm = dev_get_drvdata(dev); if (rtk_pm_get_wakeup_reason() == ALARM_EVENT) return 0; // RTC 后台唤醒 → 不恢复显示管线 return drm_mode_config_helper_resume(drm); } 11.2 HDMI 层 — rtk_hdmi_resume（兜底） rtk_hdmi.c:2296-2315\nstatic int rtk_hdmi_resume(struct device *dev) { hdmi-\u0026gt;in_suspend = false; if (rtk_pm_get_wakeup_reason() == ALARM_EVENT) return 0; // RTC 后台唤醒 → 不初始化 PHY gpiod_set_debounce(hdmi-\u0026gt;hpd_gpio, 30*1000); // ... hdmi-\u0026gt;hdmi_ops-\u0026gt;update_hpd_state(hdmi); enable_irq(hdmi-\u0026gt;hpd_irq); return 0; } 11.3 Suspend 防重入 rtk_drm_drv.c:244-249\nstatic int rtk_drm_suspend(struct device *dev) { struct drm_device *drm = dev_get_drvdata(dev); if (drm-\u0026gt;mode_config.suspend_state) // 已 suspend 过，跳过 return 0; return drm_mode_config_helper_suspend(drm); } 11.4 效果 唤醒类型 修复前 修复后 RTC 定时唤醒 PHY 上电 → TV 黑屏 PHY 保持关闭 → TV 无感知 遥控器 IR 唤醒 正常亮屏 正常亮屏 GPIO 唤醒 正常亮屏 正常亮屏 CEC 唤醒 正常亮屏 正常亮屏 涉及文件 文件 改动 drivers/gpu/drm/realtek/rtk_drm_drv.c rtk_drm_resume + rtk_drm_suspend 加判断 drivers/gpu/drm/realtek/rtk_hdmi.c rtk_hdmi_resume 加判断 drivers/soc/realtek/common/rtk_pm_common.c rtk_pm_get_wakeup_reason() 接口 include/soc/realtek/rtk_pm.h 接口声明 + ALARM_EVENT 枚举 ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/rtc%E5%94%A4%E9%86%92%E5%BC%95%E8%B5%B7%E7%9A%84%E5%BE%85%E6%9C%BA%E9%97%AE%E9%A2%98/1.rtc%E6%97%B6%E9%92%9F%E5%88%B0%E6%9C%9F%E4%B9%8B%E5%90%8E%E5%87%BA%E7%8E%B0%E9%9D%9E%E9%A2%84%E6%9C%9F%E7%9A%84hdmi%E6%9C%89%E8%BE%93%E5%87%BA/","summary":"\u003ch1 id=\"hdmi-被-rtc-唤醒--代码路径分析\"\u003eHDMI 被 RTC 唤醒 — 代码路径分析\u003c/h1\u003e\n\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003ertk-parker-aosp-squash-25q2.02-mirakulo项目\u003c/p\u003e\n\u003cp\u003e前置问题见：系统待机状态被异常唤醒.md\u003c/p\u003e\n\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003eRTC 定时唤醒 STB 时，HDMI 会短暂输出信号导致电视黑屏，然后恢复无信号。\u003c/p\u003e\n\u003cp\u003e根因：内核 PM 框架的 Resume 不判断唤醒源，无条件恢复所有设备，HDMI 驱动在 resume 时无条件打开 PHY。\u003c/p\u003e\n\u003cp\u003e修复方向：在 \u003ccode\u003ertk_drm_resume()\u003c/code\u003e 和 \u003ccode\u003ertk_hdmi_resume()\u003c/code\u003e 中判断唤醒源，RTC 后台唤醒时跳过显示/PHY 初始化。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一系统唤醒总入口\"\u003e一、系统唤醒总入口\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003ekernel/power/suspend.c:490-529\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esuspend_devices_and_enter\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003esuspend_state_t\u003c/span\u003e state)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// ① suspend 所有设备\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003edpm_suspend_start\u003c/span\u003e(PMSG_SUSPEND);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// ② SoC 深度睡眠\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003esuspend_enter\u003c/span\u003e(state, \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003ewakeup);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eResume_devices:                          \u003cspan style=\"color:#75715e\"\u003e// ③ 被 RTC/IR/GPIO 唤醒后从这里开始\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003edpm_resume_end\u003c/span\u003e(PMSG_RESUME);         \u003cspan style=\"color:#75715e\"\u003e// 恢复所有设备\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e醒来后 \u003ccode\u003edpm_resume_end()\u003c/code\u003e 是唯一出口，不管谁唤醒的都走这里。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"二dpm_resume--无差别恢复所有设备\"\u003e二、dpm_resume — 无差别恢复所有设备\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003edrivers/base/power/main.c:1001-1040\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edpm_resume\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003epm_message_t\u003c/span\u003e state)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003elist_empty\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003edpm_suspended_list)) {     \u003cspan style=\"color:#75715e\"\u003e// 遍历所有已 suspend 的设备\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        dev \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eto_device\u003c/span\u003e(dpm_suspended_list.next);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003edevice_resume\u003c/span\u003e(dev);                        \u003cspan style=\"color:#75715e\"\u003e// 逐个恢复\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003edpm_suspended_list\u003c/code\u003e 是内核 PM 框架维护的全局链表，所有被 suspend 的设备都在上面。HDMI 设备在 suspend 时被加入链表，resume 时必然被取出恢复。\u003c/p\u003e","title":"1.RTC时钟到期之后出现非预期的HDMI有输出"},{"content":"待机状态下拔插 HDMI 无输出问题 现象 STB 正常待机（suspend） 待机状态下拔掉 HDMI 线 解除待机（resume，如遥控器 IR 唤醒） 再次插入 HDMI 线 HDMI 无输出，电视显示无信号 复现步骤 待机 → 拔 HDMI → 唤醒 → 插 HDMI → 黑屏/无信号 根因：ALARM_EVENT 提前返回导致 IRQ 嵌套计数泄漏 disable_irq/enable_irq 是嵌套计数 Linux 内核的 disable_irq() / enable_irq() 使用深度计数器 desc-\u0026gt;depth，不是布尔开关：\n// kernel/irq/manage.c void disable_irq(unsigned int irq) { irq_descs[irq]-\u0026gt;depth++; } // depth \u0026gt; 0 → 中断屏蔽 void enable_irq(unsigned int irq) { irq_descs[irq]-\u0026gt;depth--; } // depth == 0 → 中断打开 必须 一对一配对，否则 depth 永不归零，中断永久丢失。\n旧代码的泄漏路径 rtk_hdmi_suspend() 无条件调用 disable_irq(hpd_irq)。rtk_hdmi_resume() 中，enable_irq(hpd_irq) 在 ALARM_EVENT 提前返回之后：\n// 旧版 rtk_hdmi_resume()： hdmi-\u0026gt;in_suspend = false; if (rtk_pm_get_wakeup_reason() == ALARM_EVENT) return 0; // ← 提前返回，跳过所有恢复 // 以下都不执行： gpiod_set_debounce(...); update_hpd_state(hdmi); enable_irq(hdmi-\u0026gt;hpd_irq); // ← 不会执行! enable_irq(hdmi-\u0026gt;rxsense_irq); // ← 不会执行! mod_timer(\u0026amp;hdmi-\u0026gt;rxsense_timer, ...); // ← 不会执行! 泄漏逐帧追溯 Suspend #1（正常待机）: disable_irq(hpd_irq) → depth: 0 → 1 Resume #1（ALARM_EVENT — RTC 定时巡检）: ALARM_EVENT → return 0 → enable_irq 没调 depth 仍 = 1 ← 泄漏! Suspend #2（巡检完毕，再次待机）: disable_irq(hpd_irq) → depth: 1 → 2 Resume #2（用户 IR 遥控器唤醒）: enable_irq(hpd_irq) → depth: 2 → 1 ← 仍然屏蔽! 用户插回 HDMI: HPD GPIO 上升沿 → IRQ 被屏蔽 → rtk_hdmi_hpd_irq() 不触发 → hpd_work 不调度 → update_hpd_state() 不调用 → drm_helper_hpd_irq_event() 不调用 → 无 modeset → 无输出 日志证据 [ 769.549851] **********rtk_drm_resume : ALARM_EVENT return 0 ********* [ 769.549857] **********ALARM_EVENT return 0 ********* ALARM_EVENT 时两份 resume 都提前返回。每 24 小时 RTC 巡检一次就泄漏一层。\n修复方案 将 enable_irq() 和 RxSense 恢复移到 ALARM_EVENT 判断之前，确保所有唤醒路径都执行中断恢复：\n// 新版 rtk_hdmi_resume()： hdmi-\u0026gt;in_suspend = false; // ① 先恢复中断（所有唤醒源都执行，避免嵌套计数泄漏） enable_irq(hdmi-\u0026gt;hpd_irq); if (hdmi-\u0026gt;rxsense_mode == RXSENSE_INTERRUPT_MODE) enable_irq(hdmi-\u0026gt;rxsense_irq); else if (hdmi-\u0026gt;rxsense_mode == RXSENSE_TIMER_MODE) mod_timer(\u0026amp;hdmi-\u0026gt;rxsense_timer, jiffies + msecs_to_jiffies(300)); // ② ALARM_EVENT 时跳过 HDMI 输出相关初始化（不读 HPD、不触发 DRM hotplug） if (rtk_pm_get_wakeup_reason() == ALARM_EVENT) return 0; // 以下仅非 ALARM 唤醒执行 gpiod_set_debounce(...); // ... HDCP 恢复 ... update_hpd_state(hdmi); return 0; 修复后的配对 Suspend #1: disable_irq(hpd_irq) → depth: 0 → 1 Resume #1 (ALARM_EVENT): enable_irq(hpd_irq) → depth: 1 → 0 ✅ 归零! Suspend #2: disable_irq(hpd_irq) → depth: 0 → 1 Resume #2 (IR_EVENT): enable_irq(hpd_irq) → depth: 1 → 0 ✅ 用户插回 HDMI: HPD IRQ 正常触发 → hotplug → modeset → 输出 ✅ DRM 侧不受影响 DRM 的 poll_enable/poll_disable 是布尔量不是计数器，不累积：\nSuspend: poll_enabled = false Resume: poll_enabled = true (ALARM 跳过也不累积，下次能正确恢复) rtk_drm_resume() 不需要修改。\n相关文件 文件 说明 kernel/rtk/parker/common-kernel/drivers/gpu/drm/realtek/rtk_hdmi.c rtk_hdmi_resume() — 修复 IRQ 恢复顺序 kernel/rtk/parker/common-kernel/drivers/gpu/drm/realtek/rtk_drm_drv.c rtk_drm_resume() — 无问题，不需改 修改历史 日期 内容 2026-06-24 将 enable_irq() 和 RxSense 恢复移到 ALARM_EVENT 判断之前 ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/rtc%E5%94%A4%E9%86%92%E5%BC%95%E8%B5%B7%E7%9A%84%E5%BE%85%E6%9C%BA%E9%97%AE%E9%A2%98/2.%E5%BE%85%E6%9C%BA%E7%8A%B6%E6%80%81%E4%B8%8B%E6%8B%94%E6%8F%92hdmi%E9%97%AE%E9%A2%98/","summary":"\u003ch1 id=\"待机状态下拔插-hdmi-无输出问题\"\u003e待机状态下拔插 HDMI 无输出问题\u003c/h1\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eSTB 正常待机（suspend）\u003c/li\u003e\n\u003cli\u003e待机状态下拔掉 HDMI 线\u003c/li\u003e\n\u003cli\u003e解除待机（resume，如遥控器 IR 唤醒）\u003c/li\u003e\n\u003cli\u003e再次插入 HDMI 线\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHDMI 无输出，电视显示无信号\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"复现步骤\"\u003e复现步骤\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e待机 → 拔 HDMI → 唤醒 → 插 HDMI → 黑屏/无信号\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"根因alarm_event-提前返回导致-irq-嵌套计数泄漏\"\u003e根因：ALARM_EVENT 提前返回导致 IRQ 嵌套计数泄漏\u003c/h2\u003e\n\u003ch3 id=\"disable_irqenable_irq-是嵌套计数\"\u003edisable_irq/enable_irq 是嵌套计数\u003c/h3\u003e\n\u003cp\u003eLinux 内核的 \u003ccode\u003edisable_irq()\u003c/code\u003e / \u003ccode\u003eenable_irq()\u003c/code\u003e 使用深度计数器 \u003ccode\u003edesc-\u0026gt;depth\u003c/code\u003e，不是布尔开关：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// kernel/irq/manage.c\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edisable_irq\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e irq)  { irq_descs[irq]\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edepth\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e; }   \u003cspan style=\"color:#75715e\"\u003e// depth \u0026gt; 0 → 中断屏蔽\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eenable_irq\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e irq)   { irq_descs[irq]\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003edepth\u003cspan style=\"color:#f92672\"\u003e--\u003c/span\u003e; }   \u003cspan style=\"color:#75715e\"\u003e// depth == 0 → 中断打开\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e必须 \u003cstrong\u003e一对一配对\u003c/strong\u003e，否则 depth 永不归零，中断永久丢失。\u003c/p\u003e\n\u003ch3 id=\"旧代码的泄漏路径\"\u003e旧代码的泄漏路径\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003ertk_hdmi_suspend()\u003c/code\u003e 无条件调用 \u003ccode\u003edisable_irq(hpd_irq)\u003c/code\u003e。\u003ccode\u003ertk_hdmi_resume()\u003c/code\u003e 中，\u003ccode\u003eenable_irq(hpd_irq)\u003c/code\u003e 在 \u003ccode\u003eALARM_EVENT\u003c/code\u003e 提前返回\u003cstrong\u003e之后\u003c/strong\u003e：\u003c/p\u003e","title":"2.待机状态下拔插HDMI问题"},{"content":"由于提前返回引起的待机状态下唤醒 HDMI 无输出 概述 为解决 RTC 定时唤醒（ALARM_EVENT）时 HDMI PHY 异常上电，在 rtk_drm_resume() 和 rtk_hdmi_resume() 中加入 ALARM_EVENT 判断后提前 return 0。这把 RTC 静默唤醒本身的 HDMI 上电问题解决了，但提前返回跳过了 resume 路径中的关键配对操作，导致跨 suspend/resume 周期的状态污染——下次手动唤醒时 HDMI 概率无输出。\n经日志验证（等待一次RTC唤醒，然后重新进入待机后，手动退出待机后HDMI无输出.txt），问题稳定复现。\ndrm_mode_config_helper_resume() 源码 rtk_drm_resume() 通过调用此函数完成 DRM 层 resume。以下为内核源码（drm_modeset_helper.c），ALARM 路径的裸 return 0 跳过了这个函数的全部三步操作。同时贴上对应的 suspend 函数作为配对参考。\n/* drm_modeset_helper.c L195-223 */ int drm_mode_config_helper_suspend(struct drm_device *dev) { struct drm_atomic_state *state; if (!dev) return 0; if (dev-\u0026gt;mode_config.poll_enabled) drm_kms_helper_poll_disable(dev); // ③ 配对: poll_disable drm_fb_helper_set_suspend_unlocked(dev-\u0026gt;fb_helper, 1); // ② 配对: fb→SUSPENDED state = drm_atomic_helper_suspend(dev); // ① 配对: 分配 state if (IS_ERR(state)) { drm_fb_helper_set_suspend_unlocked(dev-\u0026gt;fb_helper, 0); if (dev-\u0026gt;mode_config.poll_enabled) drm_kms_helper_poll_enable(dev); return PTR_ERR(state); } dev-\u0026gt;mode_config.suspend_state = state; return 0; } /* drm_modeset_helper.c L240-263 */ int drm_mode_config_helper_resume(struct drm_device *dev) { int ret; if (!dev) return 0; if (WARN_ON(!dev-\u0026gt;mode_config.suspend_state)) return -EINVAL; // 步骤 ①: 恢复原子状态 + 释放内存 ret = drm_atomic_helper_resume(dev, dev-\u0026gt;mode_config.suspend_state); if (ret) DRM_ERROR(\u0026#34;Failed to resume (%d)\\n\u0026#34;, ret); dev-\u0026gt;mode_config.suspend_state = NULL; // 步骤 ②: 恢复 fbdev drm_fb_helper_set_suspend_unlocked(dev-\u0026gt;fb_helper, 0); // 步骤 ③: 恢复输出轮询 if (dev-\u0026gt;mode_config.poll_enabled) drm_kms_helper_poll_enable(dev); return ret; } suspend 和 resume 的配对关系：\nsuspend 侧 resume 侧 drm_atomic_helper_suspend() 分配 state drm_atomic_helper_resume() 消费 + = NULL drm_fb_helper_set_suspend(fb, 1) → SUSPENDED drm_fb_helper_set_suspend(fb, 0) → RUNNING drm_kms_helper_poll_disable() drm_kms_helper_poll_enable() ALARM 路径的 return 0 跳过了 resume 侧的全部三个配对。\nrtk_drm_resume() 提前返回跳过的内容 rtk_drm_resume() 通过调用 drm_mode_config_helper_resume() 完成 resume。ALARM 路径直接 return 0，跳过了以下三步全部操作：\n步骤 ① — drm_atomic_helper_resume() + suspend_state 清理 定义位置：drm_modeset_helper.c L240-263，drm_atomic_helper.c L3369-3385\ndrm_mode_config_helper_resume(dev) ├── drm_atomic_helper_resume(dev, suspend_state) │ ├── drm_mode_config_reset(dev) → 所有 connector→status = unknown │ ├── drm_atomic_helper_commit(state) → 提交原子状态到硬件 │ │ ├── CRTC → ON → 显示管线重新启用 │ │ ├── connector → connected → connector 状态恢复 │ │ └── mode → 之前保存的分辨率 → 编码器 mode_set + enable │ └── drm_atomic_state_put(state) → 释放 suspend 时分配的内存 └── dev-\u0026gt;mode_config.suspend_state = NULL → 指针清零 跳过的具体操作 直接后果 跨周期后果 CRTC → ON 所有显示管线停留在 OFF 下次 suspend 保存 CRTC=OFF connector → connected connector 停留在 disconnected 下次 suspend 保存 connector=disconnected mode 恢复 + encoder enable 编码器不上电，无 TMDS 输出 状态被保存为\u0026quot;全禁用\u0026quot;快照 drm_atomic_state_put(state) 内存泄漏（每次 RTC 唤醒泄漏数 KB） 累积消耗 suspend_state = NULL 指针残留非 NULL 若加 guard 会被误触发 步骤 ② — drm_fb_helper_set_suspend_unlocked(fb, 0) 定义位置：drm_fb_helper.c L875-902\nsuspend 时: fb_set_suspend(fbdev, 1) → fbdev→state = FBINFO_STATE_SUSPENDED resume 时: fb_set_suspend(fbdev, 0) → fbdev→state = FBINFO_STATE_RUNNING ↑ 被跳过！ 跳过的具体操作 直接后果 跨周期后果 fbdev 恢复为 RUNNING fb 控制台不可用 下次正常 resume 时自动修复 注：console_lock 在 drm_fb_helper_set_suspend_unlocked() 内部正确配对释放（L888-L901），不存在锁泄漏。\n步骤 ③ — drm_kms_helper_poll_enable(dev) suspend 时: drm_kms_helper_poll_disable(dev) → 输出轮询停止 resume 时: drm_kms_helper_poll_enable(dev) → 输出轮询恢复 ↑ 被跳过！ 跳过的具体操作 直接后果 跨周期后果 输出轮询恢复 HPD 轮询检测失效 热插拔需依赖 IRQ（IRQ 在 hdmi 侧已恢复） rtk_hdmi_resume() 提前返回跳过的内容 注：enable_irq(hpd_irq) / enable_irq(rxsense_irq) / mod_timer(rxsense_timer) 已在 ALARM 检查之前执行，当前代码不跳过它们。以下仅列出 ALARM 检查之后被跳过的操作。\n步骤 ① — gpiod_set_debounce(hpd_gpio, 30ms) suspend 时: 未显式清除 debounce（GPIO 配置在 S3 期间保持） resume 时: gpiod_set_debounce(hpd_gpio, 30*1000) → 重新配置去抖 ↑ 被跳过！ 跳过的具体操作 直接后果 跨周期后果 HPD GPIO 去抖恢复 debounce 沿用 suspend 前配置（通常已正确） 下次正常 resume 时恢复 步骤 ② — HDCP 状态恢复 resume 时: 检测 HPD 状态 → 判定 sink_hdcp_ver → 恢复 HDCP 状态 ↑ 被跳过！ 跳过的具体操作 直接后果 跨周期后果 HDCP 版本协商 HDCP 不恢复 下次正常 resume 时完整恢复 步骤 ③ — update_hpd_state() → drm_helper_hpd_irq_event() rtk_hdmi_update_hpd_state(hdmi) ├── gpiod_get_value(hpd_gpio) → 读 HPD GPIO ├── rtk_hdmi_get_rxsense(hdmi) → 读 RxSense 电气状态 ├── hpd_state / rxsense_state 更新 → 内部状态同步 ├── extcon_set_state_sync() → 通知 extcon 框架 └── drm_helper_hpd_irq_event(dev) → 通知 DRM 核心 hotplug 事件 ↑ 全部被跳过！ 跳过的具体操作 直接后果 跨周期后果 HPD 状态同步 hdmi-\u0026gt;hpd_state 不更新 下次 resume update_hpd_state 完整执行 drm_helper_hpd_irq_event() DRM 不知道当前连接状态 connector 状态依赖 drm_atomic_helper_resume 的恢复（若该步也被跳过则状态停留在 disconnected） 跨周期污染：根因链 上述所有跳过中，致命的是 rtk_drm_resume() 跳过了步骤①（drm_atomic_helper_resume）。\n污染传播路径 ╔══════════════════════════════════════════════════════════════════╗ ║ 第一次 Suspend（RTC 唤醒之前，系统正常运行） ║ ╠══════════════════════════════════════════════════════════════════╣ ║ drm_mode_config_helper_suspend(dev) ║ ║ ├── drm_atomic_helper_suspend(dev) ║ ║ │ ├── duplicate 当前状态 → state A ║ ║ │ │ {CRTC=ON, connector=connected, mode=1080p} ║ ║ │ └── drm_atomic_helper_disable_all(dev) → CRTC OFF, 全禁用 ║ ║ ├── fb_set_suspend(fbdev, 1) → fbdev SUSPENDED ║ ║ └── suspend_state = A ║ ╚══════════════════════════════════════════════════════════════════╝ ↓ ╔══════════════════════════════════════════════════════════════════╗ ║ RTC 唤醒 → rtk_drm_resume(): ALARM_EVENT → return 0 ║ ╠══════════════════════════════════════════════════════════════════╗ ║ drm_atomic_helper_resume 跳过 ║ ║ → CRTC 仍 OFF, connector 仍 disconnected ║ ║ → suspend_state = A (未消费，残留) ║ ║ fb_set_suspend(fbdev, 0) 跳过 ║ ║ → fbdev 仍 SUSPENDED ║ ╚══════════════════════════════════════════════════════════════════╝ ↓ ╔══════════════════════════════════════════════════════════════════╗ ║ 第二次 Suspend（RTC 唤醒后系统自动重新待机） ║ ╠══════════════════════════════════════════════════════════════════╣ ║ drm_mode_config_helper_suspend(dev) ║ ║ ├── drm_atomic_helper_suspend(dev) ║ ║ │ ├── duplicate 当前状态 → state B ║ ║ │ │ {CRTC=OFF, connector=disconnected} ← 污染！ ║ ║ │ └── drm_atomic_helper_disable_all(dev) → 已是 OFF，无操作 ║ ║ ├── fb_set_suspend(fbdev, 1) → state != RUNNING → 直接返回 ║ ║ └── suspend_state = B (覆盖 A，A 泄漏) ║ ╚══════════════════════════════════════════════════════════════════╝ ↓ ╔══════════════════════════════════════════════════════════════════╗ ║ 第二次 Resume（用户手动 IR 唤醒）→ not ALARM → 全路径 ║ ╠══════════════════════════════════════════════════════════════════╣ ║ drm_mode_config_helper_resume(dev) ║ ║ ├── drm_atomic_helper_resume(dev, B) ║ ║ │ ├── drm_mode_config_reset(dev) → connector=unknown ║ ║ │ └── 提交状态 B: {CRTC=OFF, connector=disconnected} ║ ║ │ → CRTC 全部 OFF → 编码器不上电 → 无 TMDS 输出 ║ ║ ├── suspend_state = NULL ║ ║ └── fb_set_suspend(fbdev, 0) → fbdev RUNNING ║ ║ ║ ║ rtk_hdmi_resume() → update_hpd_state() ║ ║ → 读 HPD=1 → is_connected=1 → drm_helper_hpd_irq_event() ║ ║ → 但 CRTC 已 OFF，需要 userspace 响应 hotplug 才能恢复 ║ ║ ║ ║ 结果: ❌ HDMI 无输出（依赖 userspace 响应 hotplug 不稳定） ║ ╚══════════════════════════════════════════════════════════════════╝ 日志证据 等待一次RTC唤醒，然后重新进入待机后，手动退出待机后HDMI无输出.txt：\n[324.893] rtk_cec_suspend ← 第二次 suspend 开始 [324.990] [HDMI SUSPEND] hpd_irq=51 ← HDMI 进入 suspend [325.301] [DRM_RESUME] not ALARM_EVENT go ahead ← 手动唤醒，全路径 resume ← 恢复状态 B {CRTC=OFF, disconnected} // 此后没有 \u0026#34;Mode set\u0026#34; / \u0026#34;Enable encoder\u0026#34; / \u0026#34;Clear AVmute\u0026#34; // → HDMI 无输出 对比正常输出日志（进入待机后，不等待RTC唤醒，直接唤醒系统，HDMI正常输出.txt）：\n[578.676] [DRM_RESUME] not ALARM_EVENT go ahead [579.464] Mode set 1920x1080p ← 仅 788ms 后！HDMI 正常初始化 [579.466] Enable encoder [579.740] Clear AVmute 拔插恢复验证 HDMI无输出后拔插HDMI.txt 证明热插拔可以绕过污染状态恢复输出：\n[475.640] HPD(0) RxSense(1) → is_connected=0 ← 拔掉 [494.243] HPD(1) RxSense(1) → is_connected=1 ← 插回 [494.280] Mode set 1920x1080p ← DRM 完整 modeset！ [494.282] Enable encoder ← PHY 上电 [494.555] Clear AVmute ← 输出恢复 修复方案 方向 A：DRM 完整恢复 + HDMI PHY 抑制（已实现）\n核心思路：让 rtk_drm_resume() 始终走完整 drm_mode_config_helper_resume()，确保 CRTC/fbdev/connector 状态全部恢复。HDMI PHY 不上电由 rtk_hdmi_enc_enable() 中的 ALARM 检查保证。这样下次 suspend 保存的是正确状态。\n修改点 ① rtk_drm_resume() — 删除 ALARM 提前返回\n// 旧：ALARM_EVENT → return 0（跳过全部三步恢复） // 新：ALARM_EVENT 也走完整路径 return drm_mode_config_helper_resume(drm); // 始终执行 ② rtk_hdmi_enc_enable() — ALARM 时抑制 PHY 上电\nstatic void rtk_hdmi_enc_enable(struct drm_encoder *encoder) { struct rtk_hdmi *hdmi = to_rtk_hdmi(encoder); if (rtk_pm_get_wakeup_reason() == ALARM_EVENT) { dev_info(hdmi-\u0026gt;dev, \u0026#34;Skip encoder enable for ALARM_EVENT\u0026#34;); return; // CRTC 状态已恢复，但 PHY 不上电 } dev_info(hdmi-\u0026gt;dev, \u0026#34;Enable encoder\u0026#34;); rtk_hdmi_setup(hdmi, \u0026amp;hdmi-\u0026gt;previous_mode); } ③ rtk_hdmi_resume() — 维持现有逻辑不变\n// irq/timer 已在 ALARM 检查之前恢复（避免嵌套计数泄漏） // ALARM 时跳过 gpiod_set_debounce, HDCP, update_hpd_state 修复后的调用链 ALARM_EVENT 唤醒 rtk_drm_resume() └→ drm_mode_config_helper_resume() ← 完整执行 ├→ drm_atomic_helper_resume() ← CRTC ON ✅ ├→ fb_set_suspend(fb, 0) ← fbdev RUNNING ✅ └→ drm_kms_helper_poll_enable() ← poll 恢复 ✅ └→ 原子提交 → rtk_hdmi_enc_enable() └→ ALARM_EVENT? → return ← PHY 不上电 ✅ rtk_hdmi_resume() ├→ enable_irq(hpd_irq) ← IRQ 恢复 ✅ ├→ enable_irq(rxsense_irq) ← RxSense 恢复 ✅ └→ ALARM_EVENT? → return 0 ← 不调 update_hpd_state 下次 Suspend: 保存 {CRTC=ON, connector=connected} ← 正确状态 ✅ 下次 Resume: 恢复正确状态 → HDMI 正常输出 ✅ 关键原则 任何函数内提前返回时，必须保证该函数跳过的所有操作都在 suspend 侧有对应的配对操作被执行。\nsuspend 侧操作 resume 侧配对 不配对的后果 drm_atomic_helper_suspend() 分配 suspend_state drm_atomic_helper_resume() 消费 + = NULL 内存泄漏 + 指针残留 + 状态污染 drm_atomic_helper_disable_all() → CRTC OFF 原子提交 → CRTC ON 硬件停留 OFF，下次 suspend 保存禁用状态 fb_set_suspend(fbdev, 1) → SUSPENDED fb_set_suspend(fbdev, 0) → RUNNING fbdev 不可用（下次 resume 自动修复） disable_irq(hpd_irq) → depth++ enable_irq(hpd_irq) → depth\u0026ndash; IRQ 嵌套计数泄漏，永久屏蔽 del_timer_sync(rxsense_timer) mod_timer(rxsense_timer) 定时器永久停止 其中 CRTC OFF → ON 的配对是跨周期最关键的一对——跳过它导致的状态污染是 HDMI 概率无输出的根因。\n","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/rtc%E5%94%A4%E9%86%92%E5%BC%95%E8%B5%B7%E7%9A%84%E5%BE%85%E6%9C%BA%E9%97%AE%E9%A2%98/3.%E7%94%B1%E4%BA%8E%E6%8F%90%E5%89%8D%E8%BF%94%E5%9B%9E%E5%BC%95%E8%B5%B7%E7%9A%84%E5%BE%85%E6%9C%BA%E7%8A%B6%E6%80%81%E4%B8%8B%E5%94%A4%E9%86%92hdmi%E6%97%A0%E8%BE%93%E5%87%BA/","summary":"\u003ch1 id=\"由于提前返回引起的待机状态下唤醒-hdmi-无输出\"\u003e由于提前返回引起的待机状态下唤醒 HDMI 无输出\u003c/h1\u003e\n\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003e为解决 RTC 定时唤醒（ALARM_EVENT）时 HDMI PHY 异常上电，在 \u003ccode\u003ertk_drm_resume()\u003c/code\u003e 和 \u003ccode\u003ertk_hdmi_resume()\u003c/code\u003e 中加入 ALARM_EVENT 判断后提前 \u003ccode\u003ereturn 0\u003c/code\u003e。这把 RTC 静默唤醒本身的 HDMI 上电问题解决了，但提前返回跳过了 resume 路径中的关键配对操作，导致\u003cstrong\u003e跨 suspend/resume 周期的状态污染\u003c/strong\u003e——下次手动唤醒时 HDMI 概率无输出。\u003c/p\u003e\n\u003cp\u003e经日志验证（\u003ccode\u003e等待一次RTC唤醒，然后重新进入待机后，手动退出待机后HDMI无输出.txt\u003c/code\u003e），问题稳定复现。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"drm_mode_config_helper_resume-源码\"\u003edrm_mode_config_helper_resume() 源码\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003ertk_drm_resume()\u003c/code\u003e 通过调用此函数完成 DRM 层 resume。以下为内核源码（\u003ccode\u003edrm_modeset_helper.c\u003c/code\u003e），ALARM 路径的裸 \u003ccode\u003ereturn 0\u003c/code\u003e 跳过了这个函数的全部三步操作。同时贴上对应的 suspend 函数作为配对参考。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e/* drm_modeset_helper.c L195-223 */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edrm_mode_config_helper_suspend\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e drm_device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e drm_atomic_state \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003estate;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.poll_enabled)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003edrm_kms_helper_poll_disable\u003c/span\u003e(dev);                \u003cspan style=\"color:#75715e\"\u003e// ③ 配对: poll_disable\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003edrm_fb_helper_set_suspend_unlocked\u003c/span\u003e(dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003efb_helper, \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e); \u003cspan style=\"color:#75715e\"\u003e// ② 配对: fb→SUSPENDED\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    state \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edrm_atomic_helper_suspend\u003c/span\u003e(dev);                \u003cspan style=\"color:#75715e\"\u003e// ① 配对: 分配 state\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eIS_ERR\u003c/span\u003e(state)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003edrm_fb_helper_set_suspend_unlocked\u003c/span\u003e(dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003efb_helper, \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.poll_enabled)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#a6e22e\"\u003edrm_kms_helper_poll_enable\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ePTR_ERR\u003c/span\u003e(state);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.suspend_state \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e state;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e/* drm_modeset_helper.c L240-263 */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edrm_mode_config_helper_resume\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e drm_device \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e ret;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003edev)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eWARN_ON\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003edev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.suspend_state))\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eEINVAL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 步骤 ①: 恢复原子状态 + 释放内存\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ret \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edrm_atomic_helper_resume\u003c/span\u003e(dev, dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.suspend_state);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (ret)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eDRM_ERROR\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Failed to resume (%d)\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e, ret);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.suspend_state \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e NULL;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 步骤 ②: 恢复 fbdev\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003edrm_fb_helper_set_suspend_unlocked\u003c/span\u003e(dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003efb_helper, \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 步骤 ③: 恢复输出轮询\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (dev\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003emode_config.poll_enabled)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003edrm_kms_helper_poll_enable\u003c/span\u003e(dev);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e ret;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003esuspend 和 resume 的配对关系：\u003c/p\u003e","title":"3.由于提前返回引起的待机状态下唤醒HDMI无输出"},{"content":"对应到目录\nZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\bootable\\recovery\\recovery_ui\\ui.cpp 对应代码\n这里在对返回的按键做判定，判定之后根据需求返回信号\nRecoveryUI::KeyAction RecoveryUI::CheckKey(int key, bool is_long_press) { ... //这是目前reset键的判定，这个if判定成功说明按下了reset，返回了TOGGLE if ((key == KEY_VOLUMEUP || key == KEY_UP) \u0026amp;\u0026amp; IsKeyPressed(KEY_POWER)) { return TOGGLE; } ... } 在这里对返回的信号进行处理\n​\t原有的逻辑是现实/关闭UI文字，现在修改为移动索引，由于之前索引的移动是按下KEY_DOWN，所以可以直接复用逻辑，直接发送KEY_DOWN即可。另外还需要实现一个长按3s确认的功能。所以在接收到这个信号的时候进行一次判定如果是长按到来的信号，那么就走KEY_ENTER逻辑，否则就走KEY_DOWN逻辑\nvoid RecoveryUI::ProcessKey(int key_code, int updown) { ... case RecoveryUI::TOGGLE: ShowText(!IsTextVisible()); break; ... } void RecoveryUI::ProcessKey(int key_code, int updown) { ... case RecoveryUI::TOGGLE: ShowText(true); if (long_press) { EnqueueKey(KEY_ENTER); // 长按确认 } else { EnqueueKey(KEY_DOWN); // 短按下移 } break; ... } 另外对应到长按的逻辑在\nvoid RecoveryUI::TimeKey(int key_code, int count) { ... std::this_thread::sleep_for(750ms); // 750 ms == \u0026#34;long\u0026#34; ... } ","permalink":"https://luojmarkdown.pages.dev/posts/%E9%A1%B9%E7%9B%AE%E9%97%AE%E9%A2%98/recovery%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84reset%E9%80%BB%E8%BE%91%E4%BF%AE%E6%94%B9/","summary":"\u003cp\u003e对应到目录\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eZ:\\android\\aml\\s905x5\\aml-s905x5-androidu-v2\\bootable\\recovery\\recovery_ui\\ui.cpp\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e对应代码\u003c/p\u003e\n\u003cp\u003e这里在对返回的按键做判定，判定之后根据需求返回信号\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eRecoveryUI::KeyAction RecoveryUI::CheckKey(int key, bool is_long_press) \n{\n    ...\n    //这是目前reset键的判定，这个if判定成功说明按下了reset，返回了TOGGLE\n    if ((key == KEY_VOLUMEUP || key == KEY_UP) \u0026amp;\u0026amp; IsKeyPressed(KEY_POWER)) \n    {\n      return TOGGLE;\n    }\n    ...\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在这里对返回的信号进行处理\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t原有的逻辑是现实/关闭UI文字，现在修改为移动索引，由于之前索引的移动是按下KEY_DOWN，所以可以直接复用逻辑，直接发送KEY_DOWN即可。另外还需要实现一个长按3s确认的功能。所以在接收到这个信号的时候进行一次判定如果是长按到来的信号，那么就走KEY_ENTER逻辑，否则就走KEY_DOWN逻辑\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003evoid RecoveryUI::ProcessKey(int key_code, int updown) \n{\n    ...\n    case RecoveryUI::TOGGLE:\n        ShowText(!IsTextVisible());\n        break;\n    ...\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003evoid RecoveryUI::ProcessKey(int key_code, int updown) \n{\n    ...\n    case RecoveryUI::TOGGLE:\n        ShowText(true);\n        if (long_press) \n        {\n            EnqueueKey(KEY_ENTER);  // 长按确认\n        } \n    \telse\n        {\n            EnqueueKey(KEY_DOWN);   // 短按下移\n        }\n        break;\n    ...\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e另外对应到长按的逻辑在\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003evoid RecoveryUI::TimeKey(int key_code, int count) \n{\n  ...\n  std::this_thread::sleep_for(750ms);  // 750 ms == \u0026#34;long\u0026#34;\n  ...\n}\n\u003c/code\u003e\u003c/pre\u003e","title":"recovery模式下的reset逻辑修改"},{"content":"Android 安全基础 参考指导书 6.9 安全基础\n学习清单 数据加密（EncryptedSharedPreferences / Crypto） HTTPS 证书校验 代码混淆（ProGuard / R8） 敏感信息保护（API Key 等） WebView 安全 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/09-%E5%AE%89%E5%85%A8%E5%9F%BA%E7%A1%80/","summary":"\u003ch1 id=\"android-安全基础\"\u003eAndroid 安全基础\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.9 安全基础\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 数据加密（EncryptedSharedPreferences / Crypto）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e HTTPS 证书校验\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 代码混淆（ProGuard / R8）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 敏感信息保护（API Key 等）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e WebView 安全\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"09-Android安全基础"},{"content":"Android 性能优化 参考指导书 6.8 性能优化\n学习清单 布局优化（减少层级、ViewStub, merge） 内存泄漏检测（LeakCanary） 内存优化（避免不必要的对象创建） 启动速度优化（冷启动 / 热启动） APK 体积优化 ANR 问题定位与排查 Profiler 工具使用 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/08-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/","summary":"\u003ch1 id=\"android-性能优化\"\u003eAndroid 性能优化\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.8 性能优化\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 布局优化（减少层级、ViewStub, merge）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 内存泄漏检测（LeakCanary）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 内存优化（避免不必要的对象创建）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 启动速度优化（冷启动 / 热启动）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e APK 体积优化\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ANR 问题定位与排查\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Profiler 工具使用\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"08-Android性能优化"},{"content":"Jetpack Compose 参考指导书 6.7 Jetpack Compose\n学习清单 Compose 基本概念（声明式 UI） Composable 函数 布局（Column, Row, Box, ConstraintLayout） 状态管理（remember, mutableStateOf, StateFlow 整合） Modifier 修饰符 Compose 中导航 Compose 与 View 体系互操作 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/07-jetpackcompose/","summary":"\u003ch1 id=\"jetpack-compose\"\u003eJetpack Compose\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.7 Jetpack Compose\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Compose 基本概念（声明式 UI）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Composable 函数\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 布局（Column, Row, Box, ConstraintLayout）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 状态管理（remember, mutableStateOf, StateFlow 整合）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Modifier 修饰符\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Compose 中导航\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Compose 与 View 体系互操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"07-JetpackCompose"},{"content":"Jetpack 组件 参考指导书 6.6 Jetpack 组件\n学习清单 Navigation 导航组件 Room 数据库 ViewModel + LiveData DataStore WorkManager（后台任务调度） Paging 3（分页加载） CameraX（相机） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/06-jetpack%E7%BB%84%E4%BB%B6/","summary":"\u003ch1 id=\"jetpack-组件\"\u003eJetpack 组件\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.6 Jetpack 组件\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Navigation 导航组件\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Room 数据库\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ViewModel + LiveData\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e DataStore\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e WorkManager（后台任务调度）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Paging 3（分页加载）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e CameraX（相机）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"06-Jetpack组件"},{"content":"Android 依赖注入 参考指导书 6.5 依赖注入\n学习清单 依赖注入概念 Dagger 基础（了解） Hilt（Android 官方推荐 DI 框架） Koin（轻量级替代方案，可选） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/05-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5/","summary":"\u003ch1 id=\"android-依赖注入\"\u003eAndroid 依赖注入\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.5 依赖注入\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 依赖注入概念\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Dagger 基础（了解）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Hilt（Android 官方推荐 DI 框架）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Koin（轻量级替代方案，可选）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"05-Android依赖注入"},{"content":"Android 异步处理 参考指导书 6.4 异步处理\n学习清单 主线程与子线程 Handler 与 Looper 机制 AsyncTask（已废弃，了解即可） RxJava 基础（可选了解） Kotlin Coroutines 在 Android 中的最佳实践 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/04-%E5%BC%82%E6%AD%A5%E5%A4%84%E7%90%86/","summary":"\u003ch1 id=\"android-异步处理\"\u003eAndroid 异步处理\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.4 异步处理\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 主线程与子线程\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Handler 与 Looper 机制\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e AsyncTask（已废弃，了解即可）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e RxJava 基础（可选了解）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Kotlin Coroutines 在 Android 中的最佳实践\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"04-Android异步处理"},{"content":"Android 图片加载 参考指导书 6.3 图片加载\n学习清单 Glide 图片加载库 Coil（Kotlin 原生，推荐） 图片缓存策略 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/03-%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD/","summary":"\u003ch1 id=\"android-图片加载\"\u003eAndroid 图片加载\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.3 图片加载\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Glide 图片加载库\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Coil（Kotlin 原生，推荐）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 图片缓存策略\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"03-Android图片加载"},{"content":"Android 网络请求 参考指导书 6.2 网络请求\n学习清单 权限声明（INTERNET） 网络请求基本概念（RESTful API, JSON） OkHttp 基础使用 Retrofit 网络框架 Gson / Moshi JSON 解析 网络请求最佳实践（单例、拦截器、缓存） 网络安全配置（Network Security Config） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/02-%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82/","summary":"\u003ch1 id=\"android-网络请求\"\u003eAndroid 网络请求\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.2 网络请求\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 权限声明（INTERNET）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 网络请求基本概念（RESTful API, JSON）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e OkHttp 基础使用\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Retrofit 网络框架\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Gson / Moshi JSON 解析\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 网络请求最佳实践（单例、拦截器、缓存）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 网络安全配置（Network Security Config）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"02-Android网络请求"},{"content":"Android 架构模式 参考指导书 6.1 架构模式\n学习清单 MVC 模式理解 MVP 模式理解 MVVM 模式（Android 推荐） ViewModel 的使用与原理 LiveData / StateFlow 数据观察 DataBinding 数据绑定 ViewBinding 视图绑定 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/5.android%E8%BF%9B%E9%98%B6/01-%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F/","summary":"\u003ch1 id=\"android-架构模式\"\u003eAndroid 架构模式\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 6.1 架构模式\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e MVC 模式理解\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e MVP 模式理解\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e MVVM 模式（Android 推荐）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ViewModel 的使用与原理\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e LiveData / StateFlow 数据观察\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e DataBinding 数据绑定\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ViewBinding 视图绑定\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"01-Android架构模式"},{"content":"Android 数据存储 参考指导书 5.6 数据存储\n学习清单 SharedPreferences（轻量键值对存储） DataStore（SharedPreferences 替代方案） 文件存储（内部存储 / 外部存储） SQLite 数据库 Room 持久化库（推荐） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/4.android%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/06-%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/","summary":"\u003ch1 id=\"android-数据存储\"\u003eAndroid 数据存储\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 5.6 数据存储\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e SharedPreferences（轻量键值对存储）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e DataStore（SharedPreferences 替代方案）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 文件存储（内部存储 / 外部存储）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e SQLite 数据库\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Room 持久化库（推荐）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"06-Android数据存储"},{"content":"ContentProvider 内容提供者 参考指导书 5.5 ContentProvider\n学习清单 ContentProvider 概念与用途 ContentResolver 访问系统数据（通讯录、日历等） 自定义 ContentProvider 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/4.android%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/05-%E5%86%85%E5%AE%B9%E6%8F%90%E4%BE%9B%E8%80%85/","summary":"\u003ch1 id=\"contentprovider-内容提供者\"\u003eContentProvider 内容提供者\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 5.5 ContentProvider\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ContentProvider 概念与用途\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ContentResolver 访问系统数据（通讯录、日历等）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 自定义 ContentProvider\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"05-ContentProvider内容提供者"},{"content":"BroadcastReceiver 广播接收器 参考指导书 5.4 BroadcastReceiver\n学习清单 广播类型（标准广播、有序广播、粘性广播） 静态注册与动态注册 系统广播（网络变化、电量、开机等） 自定义广播 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/4.android%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/04-%E5%B9%BF%E6%92%AD%E6%8E%A5%E6%94%B6%E5%99%A8/","summary":"\u003ch1 id=\"broadcastreceiver-广播接收器\"\u003eBroadcastReceiver 广播接收器\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 5.4 BroadcastReceiver\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 广播类型（标准广播、有序广播、粘性广播）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 静态注册与动态注册\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 系统广播（网络变化、电量、开机等）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 自定义广播\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"04-BroadcastReceiver广播接收器"},{"content":"Service 服务 参考指导书 5.3 Service\n学习清单 Service 生命周期 启动服务（startService） 绑定服务（bindService） IntentService（已废弃，了解即可） 前台服务（Foreground Service）与通知栏 后台任务限制（Android 8.0+） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/4.android%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/03-service%E6%9C%8D%E5%8A%A1/","summary":"\u003ch1 id=\"service-服务\"\u003eService 服务\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 5.3 Service\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Service 生命周期\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 启动服务（startService）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 绑定服务（bindService）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e IntentService（已废弃，了解即可）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 前台服务（Foreground Service）与通知栏\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 后台任务限制（Android 8.0+）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"03-Service服务"},{"content":"Fragment 参考指导书 5.2 Fragment\n学习清单 Fragment 生命周期 Fragment 创建与使用（FragmentManager） Fragment 与 Activity 通信 Navigation 组件与 Fragment 结合 DialogFragment 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/4.android%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/02-fragment/","summary":"\u003ch1 id=\"fragment\"\u003eFragment\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 5.2 Fragment\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Fragment 生命周期\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Fragment 创建与使用（FragmentManager）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Fragment 与 Activity 通信\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Navigation 组件与 Fragment 结合\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e DialogFragment\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"02-Fragment"},{"content":"Activity 深入 参考指导书 5.1 Activity 深入\n学习清单 Activity 生命周期完整理解 启动模式（standard, singleTop, singleTask, singleInstance） onSaveInstanceState 状态保存与恢复 Activity 之间数据传递（Intent Bundle） startActivityForResult / ActivityResultContracts 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/4.android%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/01-activity%E6%B7%B1%E5%85%A5/","summary":"\u003ch1 id=\"activity-深入\"\u003eActivity 深入\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 5.1 Activity 深入\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Activity 生命周期完整理解\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 启动模式（standard, singleTop, singleTask, singleInstance）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e onSaveInstanceState 状态保存与恢复\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Activity 之间数据传递（Intent Bundle）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e startActivityForResult / ActivityResultContracts\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"01-Activity深入"},{"content":"Android 样式与资源 参考指导书 4.4 样式与资源\n学习清单 Style 样式定义与继承 Material Design 组件（MaterialButton, Chip, CardView 等） Drawable（shape, layer-list, selector） 屏幕适配基础（dp, sp, px 区别） Vector Drawable 矢量图 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/3.android%E5%9F%BA%E7%A1%80/04-%E6%A0%B7%E5%BC%8F%E4%B8%8E%E8%B5%84%E6%BA%90/","summary":"\u003ch1 id=\"android-样式与资源\"\u003eAndroid 样式与资源\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 4.4 样式与资源\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Style 样式定义与继承\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Material Design 组件（MaterialButton, Chip, CardView 等）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Drawable（shape, layer-list, selector）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 屏幕适配基础（dp, sp, px 区别）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Vector Drawable 矢量图\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"04-Android样式与资源"},{"content":"Android UI 基础（View 体系） 参考指导书 4.3 UI 基础\n学习清单 布局管理器 LinearLayout RelativeLayout ConstraintLayout（推荐） FrameLayout TableLayout 常用控件 TextView, EditText Button, ImageButton ImageView CheckBox, RadioButton Switch, ToggleButton ProgressBar, SeekBar RatingBar ScrollView 与 NestedScrollView RecyclerView（列表的核心控件） ViewPager（页面滑动） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/3.android%E5%9F%BA%E7%A1%80/03-ui%E5%9F%BA%E7%A1%80/","summary":"\u003ch1 id=\"android-ui-基础view-体系\"\u003eAndroid UI 基础（View 体系）\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 4.3 UI 基础\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 布局管理器\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e LinearLayout\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e RelativeLayout\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ConstraintLayout（推荐）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e FrameLayout\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e TableLayout\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 常用控件\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e TextView, EditText\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Button, ImageButton\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ImageView\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e CheckBox, RadioButton\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Switch, ToggleButton\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ProgressBar, SeekBar\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e RatingBar\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ScrollView 与 NestedScrollView\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e RecyclerView（列表的核心控件）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e ViewPager（页面滑动）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"03-AndroidUI基础（View体系）"},{"content":"Android 应用基础 参考指导书 4.2 应用基础\n学习清单 AndroidManifest.xml 清单文件 Activity 与生命周期 Intent（显式 / 隐式） 页面导航与数据传递 资源管理（res/ 目录） 字符串与多语言（strings.xml） 颜色、尺寸、主题（colors.xml, dimens.xml, themes.xml） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/3.android%E5%9F%BA%E7%A1%80/02-%E5%BA%94%E7%94%A8%E5%9F%BA%E7%A1%80/","summary":"\u003ch1 id=\"android-应用基础\"\u003eAndroid 应用基础\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 4.2 应用基础\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e AndroidManifest.xml 清单文件\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Activity 与生命周期\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Intent（显式 / 隐式）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 页面导航与数据传递\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 资源管理（res/ 目录）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 字符串与多语言（strings.xml）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 颜色、尺寸、主题（colors.xml, dimens.xml, themes.xml）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"02-Android应用基础"},{"content":"Android 开发环境 参考指导书 4.1 开发环境\n学习清单 Android Studio 安装与配置 Android SDK 管理 AVD（Android Virtual Device）创建 Gradle 构建系统基础 项目目录结构理解 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/3.android%E5%9F%BA%E7%A1%80/01-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/","summary":"\u003ch1 id=\"android-开发环境\"\u003eAndroid 开发环境\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 4.1 开发环境\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Android Studio 安装与配置\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Android SDK 管理\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e AVD（Android Virtual Device）创建\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Gradle 构建系统基础\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 项目目录结构理解\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"01-Android开发环境"},{"content":"Kotlin 协程 参考指导书 3.6 Kotlin 协程\n学习清单 协程基础（suspend 函数） 协程构建器（launch, async, runBlocking） 协程上下文与调度器（Dispatchers） 协程作用域（CoroutineScope, viewModelScope, lifecycleScope） 通道（Channel）与流（Flow） 异常处理（try-catch, CoroutineExceptionHandler） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/2.kotlin%E5%9F%BA%E7%A1%80/06-%E5%8D%8F%E7%A8%8B/","summary":"\u003ch1 id=\"kotlin-协程\"\u003eKotlin 协程\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 3.6 Kotlin 协程\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 协程基础（suspend 函数）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 协程构建器（launch, async, runBlocking）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 协程上下文与调度器（Dispatchers）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 协程作用域（CoroutineScope, viewModelScope, lifecycleScope）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 通道（Channel）与流（Flow）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 异常处理（try-catch, CoroutineExceptionHandler）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"06-Kotlin协程"},{"content":"Kotlin 集合与序列 参考指导书 3.5 集合与序列\n学习清单 List, Set, Map（可变与不可变） 集合操作符（map, filter, reduce, flatMap） 序列（Sequence） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/2.kotlin%E5%9F%BA%E7%A1%80/05-%E9%9B%86%E5%90%88%E4%B8%8E%E5%BA%8F%E5%88%97/","summary":"\u003ch1 id=\"kotlin-集合与序列\"\u003eKotlin 集合与序列\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 3.5 集合与序列\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e List, Set, Map（可变与不可变）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 集合操作符（map, filter, reduce, flatMap）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 序列（Sequence）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"05-Kotlin集合与序列"},{"content":"Kotlin 空安全 参考指导书 3.4 空安全\n学习清单 可空类型（?） 安全调用操作符（?.） Elvis 操作符（?:） 非空断言（!!） let / run / apply / also / with 作用域函数 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/2.kotlin%E5%9F%BA%E7%A1%80/04-%E7%A9%BA%E5%AE%89%E5%85%A8/","summary":"\u003ch1 id=\"kotlin-空安全\"\u003eKotlin 空安全\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 3.4 空安全\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 可空类型（?）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 安全调用操作符（?.）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Elvis 操作符（?:）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 非空断言（!!）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e let / run / apply / also / with 作用域函数\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"04-Kotlin空安全"},{"content":"Kotlin 面向对象（Kotlin 风格） 参考指导书 3.3 面向对象\n学习清单 类与构造函数（主构造函数、次构造函数） init 初始化块 属性（getter/setter） 继承与重写（open, override） 抽象类与接口 数据类（data class） 密封类（sealed class） 对象（object）与伴生对象（companion object） 委托（by 关键字） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/2.kotlin%E5%9F%BA%E7%A1%80/03-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/","summary":"\u003ch1 id=\"kotlin-面向对象kotlin-风格\"\u003eKotlin 面向对象（Kotlin 风格）\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 3.3 面向对象\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 类与构造函数（主构造函数、次构造函数）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e init 初始化块\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 属性（getter/setter）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 继承与重写（open, override）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 抽象类与接口\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 数据类（data class）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 密封类（sealed class）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 对象（object）与伴生对象（companion object）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 委托（by 关键字）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"03-Kotlin面向对象（Kotlin风格）"},{"content":"Kotlin 函数 参考指导书 3.2 函数\n学习清单 函数声明与调用 默认参数与命名参数 单表达式函数 扩展函数（Extension Function） 高阶函数（Higher-Order Function） Lambda 表达式 内联函数（inline） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/2.kotlin%E5%9F%BA%E7%A1%80/02-%E5%87%BD%E6%95%B0/","summary":"\u003ch1 id=\"kotlin-函数\"\u003eKotlin 函数\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 3.2 函数\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 函数声明与调用\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 默认参数与命名参数\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 单表达式函数\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 扩展函数（Extension Function）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 高阶函数（Higher-Order Function）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Lambda 表达式\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 内联函数（inline）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"02-Kotlin函数"},{"content":"Kotlin 基础语法 参考指导书 3.1 基础语法\n学习清单 Kotlin 环境搭建（IntelliJ IDEA / Android Studio） 变量声明（val vs var） 基本数据类型 字符串模板 条件表达式（if-else, when） 循环（for, while, do-while） 区间（Range） 笔记 在此记录学习笔记\u0026hellip;\n代码 实操代码放在 src/ 目录下\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/2.kotlin%E5%9F%BA%E7%A1%80/01-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95/","summary":"\u003ch1 id=\"kotlin-基础语法\"\u003eKotlin 基础语法\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 3.1 基础语法\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e Kotlin 环境搭建（IntelliJ IDEA / Android Studio）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 变量声明（val vs var）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 基本数据类型\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 字符串模板\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 条件表达式（if-else, when）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 循环（for, while, do-while）\u003c/li\u003e\n\u003cli\u003e\u003cinput disabled=\"\" type=\"checkbox\"\u003e 区间（Range）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"笔记\"\u003e笔记\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在此记录学习笔记\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"代码\"\u003e代码\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e实操代码放在 \u003ccode\u003esrc/\u003c/code\u003e 目录下\u003c/p\u003e\n\u003c/blockquote\u003e","title":"01-Kotlin基础语法"},{"content":"Java 进阶特性 参考指导书 2.4 Java 进阶特性\n学习清单 异常处理（try-catch-finally, throws, 自定义异常） IO 流（File, InputStream, OutputStream, Reader, Writer） 多线程（Thread, Runnable, 线程池 ExecutorService） Lambda 表达式（Java 8+） Stream API（Java 8+） 注解（Annotation） 反射（Reflection） 1. 异常处理 1.1 为什么需要异常 没有异常机制的语言（比如 C）靠返回值来判断错误：\nint result = doSomething(); if (result == -1) { /* 错误 1 */ } if (result == -2) { /* 错误 2 */ } // 正常逻辑和错误处理混在一起，层层 if 嵌套，可读性极差 Java 的异常机制把正常逻辑和错误处理分开——代码沿着\u0026quot;快乐路径\u0026quot;写，错误交给 catch 块处理。\n1.2 异常体系 Throwable ├── Error 严重错误，程序无法处理（OutOfMemoryError、StackOverflowError） └── Exception 可处理的异常 ├── RuntimeException（运行期异常） 不强制处理，如 NullPointerException、ArrayIndexOutOfBoundsException └── 其他（编译期异常） 必须显式处理，如 IOException、SQLException 编译期异常 vs 运行期异常：\n编译期异常 (Checked) 运行期异常 (Unchecked) 父类 Exception（非 RuntimeException） RuntimeException 编译器 强制你处理（try-catch 或 throws） 不强制 典型例子 IOException, SQLException NullPointerException, ArrayIndexOutOfBounds 设计意图 外部因素导致，可以预见和恢复 程序 bug，应该修复代码而非捕获 1.3 基本语法 try { // 可能出异常的代码 int result = 10 / 0; } catch (ArithmeticException e) { // 捕获特定异常 System.out.println(\u0026#34;除数不能为 0！\u0026#34;); e.printStackTrace(); // 打印调用栈，调试用 } finally { // 无论是否异常，都会执行（常用于关闭资源） System.out.println(\u0026#34;finally 一定会执行\u0026#34;); } 执行流程：try 出异常 → 跳过 try 剩余代码 → 匹配 catch → 执行 finally。try 没出异常 → 跳过 catch → 执行 finally。\n1.4 finally 的一个特例 // 唯一不执行 finally 的情况：JVM 退出 try { System.exit(0); // JVM 直接退出 } finally { System.out.println(\u0026#34;这行不会执行\u0026#34;); } 1.5 throws：把异常往上抛 // 自己不处理，声明\u0026#34;我可能抛这个异常\u0026#34;，让调用方处理 public void readFile(String path) throws IOException { FileReader reader = new FileReader(path); // ... } 选择原则：当前方法能处理就用 try-catch；处理不了（不知道该怎么恢复）就用 throws 往上抛。\n1.6 自定义异常 class AgeException extends Exception { public AgeException(String msg) { super(msg); } } public void setAge(int age) throws AgeException { if (age \u0026lt; 0 || age \u0026gt; 150) { throw new AgeException(\u0026#34;年龄不合法：\u0026#34; + age); } this.age = age; } 自定义异常的目的：让异常名本身就表达语义。AgeException 比 IllegalArgumentException 更有表达力。\n1.7 try-with-resources（Java 7+） // 旧写法：手动关闭，嵌套冗长 FileReader reader = null; try { reader = new FileReader(\u0026#34;file.txt\u0026#34;); // ... } finally { if (reader != null) reader.close(); // 关闭也很啰嗦 } // 新写法：自动关闭 try (FileReader reader = new FileReader(\u0026#34;file.txt\u0026#34;)) { // ... } // 自动调用 reader.close()，不用写 finally 任何实现了 AutoCloseable 接口的类都可以用在 try-with-resources 中。\n2. IO 流 2.1 流的基本概念 Java 把数据的输入/输出抽象为\u0026quot;流（Stream）\u0026quot;：\n程序 ←──InputStream/Reader── 【数据源：文件、网络、键盘...】 程序 ──→OutputStream/Writer── 【数据目标：文件、网络、屏幕...】 字节流（二进制） 字符流（文本） 输入 InputStream Reader 输出 OutputStream Writer 文件实现 FileInputStream / FileOutputStream FileReader / FileWriter 为什么分字节流和字符流？ 字节流按 byte 读写，字符流按 char 读写 + 自动处理字符编码。读文本文件用字符流，读图片/视频用字节流。\n2.2 File 类 File file = new File(\u0026#34;test.txt\u0026#34;); file.exists() // 文件是否存在 file.getName() // \u0026#34;test.txt\u0026#34; file.getAbsolutePath() // 绝对路径 file.length() // 文件大小（字节） file.isFile() // 是文件还是目录 file.isDirectory() file.createNewFile(); // 创建文件 file.delete(); // 删除 file.mkdir(); // 创建单层目录 file.mkdirs(); // 创建多层目录 2.3 文件读写（字符流） // 写文件 try (FileWriter writer = new FileWriter(\u0026#34;output.txt\u0026#34;)) { writer.write(\u0026#34;Hello, Java IO!\u0026#34;); } // 读文件（逐字符） try (FileReader reader = new FileReader(\u0026#34;output.txt\u0026#34;)) { int ch; while ((ch = reader.read()) != -1) { System.out.print((char) ch); } } 2.4 缓冲流：提高效率 // 不加缓冲：每读一个字符就访问一次磁盘 → 极慢 // 加缓冲：一次读一大块到内存缓冲区 → 快得多 try (BufferedReader reader = new BufferedReader(new FileReader(\u0026#34;large.txt\u0026#34;))) { String line; while ((line = reader.readLine()) != null) { // 逐行读 System.out.println(line); } } BufferedReader 和 BufferedWriter 就是给普通 Reader/Writer 套了一层缓冲区，减少磁盘访问次数。\n3. 多线程 3.1 为什么需要多线程 单线程程序同一时间只能做一件事。如果主线程在等待网络响应（500ms），整个程序就卡住 500ms。\n多线程让程序\u0026quot;同时\u0026quot;做多件事——一个线程等网络，另一个线程继续响应用户操作。这就是为什么你的手机不会因为下载文件而卡死。\n3.2 创建线程的方式 方式一：继承 Thread\nclass MyThread extends Thread { @Override public void run() { System.out.println(\u0026#34;新线程运行中: \u0026#34; + Thread.currentThread().getName()); } } MyThread t = new MyThread(); t.start(); // 启动线程（JVM 会调用 run()） // t.run(); 错误！直接调用 run() 只是普通方法调用，不会启动新线程 方式二：实现 Runnable（推荐）\nclass MyTask implements Runnable { @Override public void run() { System.out.println(\u0026#34;任务执行中...\u0026#34;); } } Thread t = new Thread(new MyTask()); t.start(); 用 Runnable 而不是 Thread 的原因：Java 单继承——继承 Thread 就不能再继承别的类。实现 Runnable 更灵活，还能把任务提交给线程池。\n3.3 start() vs run() t.start(); // 启动一个新线程，在新线程中执行 run() t.run(); // 在当前线程中调用 run()，没有新线程！ 这是初学者最常犯的错误。start() 是新线程并行执行；run() 是同步调用。\n3.4 线程常用方法 Thread.sleep(1000); // 让当前线程休眠 1 秒（可能抛 InterruptedException） t.join(); // 等待线程 t 执行完毕 Thread.currentThread() // 获取当前线程对象 t.setPriority(Thread.MAX_PRIORITY); // 设置优先级（1~10，只是建议，不保证） 3.5 线程安全问题 两个线程同时操作同一个变量时，结果不可预测：\nclass Counter { private int count = 0; public void increment() { count++; // 这行代码实际分三步：读 → 加 1 → 写回 } // 线程 A 读完后线程 B 插进来改 → A 的写入覆盖了 B 的结果 } // 两个线程各执行 10000 次 increment，最终 count 可能小于 20000 用 synchronized 加锁：\nclass Counter { private int count = 0; public synchronized void increment() { // 同一时刻只能一个线程进入 count++; } } synchronized 保证了原子性——方法内的操作作为一个整体，不会被其他线程打断。\n3.6 线程池（ExecutorService） 每次都 new Thread().start() 的问题：创建/销毁线程开销大、线程数不可控（来 10000 个请求就建 10000 个线程 → 系统崩）。\n线程池 = 固定数量的\u0026quot;常驻\u0026quot;线程，任务来了排队等：\nExecutorService pool = Executors.newFixedThreadPool(3); // 创建 3 个常驻线程 for (int i = 0; i \u0026lt; 10; i++) { final int taskId = i; pool.submit(() -\u0026gt; { System.out.println(\u0026#34;任务 \u0026#34; + taskId + \u0026#34; 由 \u0026#34; + Thread.currentThread().getName() + \u0026#34; 执行\u0026#34;); Thread.sleep(1000); return null; }); } pool.shutdown(); // 不再接收新任务，已有任务执行完就关闭 10 个任务，3 个线程，每个线程排队轮流执行——线程数可控，不会无限膨胀。\n4. Lambda 表达式 4.1 Lambda 解决什么问题 回顾匿名内部类的痛点——为了实现一个简单逻辑，被迫写一堆样板代码：\n// 匿名内部类：就为了排序逻辑，写了 5 行 list.sort(new Comparator\u0026lt;String\u0026gt;() { @Override public int compare(String a, String b) { return a.compareTo(b); } }); // Lambda：同样的事，1 行 list.sort((a, b) -\u0026gt; a.compareTo(b)); Lambda 就是\u0026quot;把方法当参数传\u0026quot;——让代码更简洁，只保留核心逻辑，去掉样板。\n4.2 语法 (参数) -\u0026gt; { 方法体 } 演变过程：\n// 完整版 (String a, String b) -\u0026gt; { return a.compareTo(b); } // 去掉参数类型（编译器能推断） (a, b) -\u0026gt; { return a.compareTo(b); } // 方法体只有一行时，去掉 {} 和 return (a, b) -\u0026gt; a.compareTo(b); // 如果只有一个参数，连 () 都可以省略 name -\u0026gt; System.out.println(name); // 无参数 () -\u0026gt; System.out.println(\u0026#34;Hello\u0026#34;); 4.3 Lambda 的条件 Lambda 只能用在函数式接口上—— 有且仅有一个抽象方法的接口：\n@FunctionalInterface // 注解：编译器检查是否只有一个抽象方法 interface Calculator { int calc(int a, int b); // 只有一个抽象方法 → 可以用 Lambda } Calculator add = (a, b) -\u0026gt; a + b; Calculator mul = (a, b) -\u0026gt; a * b; System.out.println(add.calc(3, 5)); // 8 System.out.println(mul.calc(3, 5)); // 15 JDK 内置的常见函数式接口：Runnable、Comparator、ActionListener。这些都是单抽象方法接口，都可以用 Lambda 简化。\n5. Stream API 5.1 为什么需要 Stream 对集合做\u0026quot;过滤 + 转换 + 汇总\u0026quot;这种操作，传统的 for 循环又长又啰嗦：\n// 任务：从成绩表中筛选出及格（≥60）的学生姓名，按字母排序 List\u0026lt;Student\u0026gt; students = ...; // 传统方式：6 步，中间变量一堆 List\u0026lt;String\u0026gt; passed = new ArrayList\u0026lt;\u0026gt;(); for (Student s : students) { if (s.getScore() \u0026gt;= 60) { passed.add(s.getName()); } } Collections.sort(passed); // Stream 方式：一条流水线 List\u0026lt;String\u0026gt; passed = students.stream() .filter(s -\u0026gt; s.getScore() \u0026gt;= 60) // 筛选 .map(Student::getName) // 提取姓名 .sorted() // 排序 .collect(Collectors.toList()); // 收集结果 Stream 把\u0026quot;对集合的多次操作\u0026quot;变成一条流水线——数据从上游流到下游，每经过一个操作就变换一次。\n5.2 三种操作 数据源 ──▶ 中间操作（懒执行） ──▶ 终端操作（触发执行） ──▶ 结果 .stream() filter/map/sorted... collect/count/forEach... 中间操作不触发计算——它们只是\u0026quot;记下要做什么\u0026quot;。只有终端操作出现时，数据才真正开始流动。这叫惰性求值，避免中间结果的浪费。\n5.3 常用操作速查 List\u0026lt;Integer\u0026gt; nums = Arrays.asList(1, 2, 3, 4, 5, 6); // 中间操作 nums.stream() .filter(n -\u0026gt; n % 2 == 0) // 过滤出偶数 → [2, 4, 6] .map(n -\u0026gt; n * 10) // 每个×10 → [20, 40, 60] .sorted((a, b) -\u0026gt; b - a) // 降序排列 → [60, 40, 20] .limit(2) // 只要前2个 → [60, 40] .forEach(System.out::println); // 终端操作：打印每个元素 操作 类型 作用 示例 filter 中间 保留满足条件的 .filter(n -\u0026gt; n \u0026gt; 0) map 中间 转换每个元素 .map(s -\u0026gt; s.length()) sorted 中间 排序 .sorted() distinct 中间 去重 .distinct() limit 中间 截断前 N 个 .limit(10) collect 终端 收集为集合 .collect(Collectors.toList()) count 终端 计数 .count() forEach 终端 遍历每个元素 .forEach(System.out::println) 6. 注解 6.1 什么是注解 注解 = 给代码贴标签。它本身不改变程序逻辑，但编译器和框架会读取这些标签做额外处理。\n@Override // 告诉编译器：我在重写父类方法，请帮我检查 @Deprecated // 标记这个方法已过时，调用会出编译警告 @FunctionalInterface // 标记这是函数式接口，有多个抽象方法就报错 6.2 自定义注解 // 定义一个注解 @Retention(RetentionPolicy.RUNTIME) // 保留到运行时（否则编译后就丢了） @Target(ElementType.METHOD) // 只能用在方法上 @interface MyTest { String value() default \u0026#34;\u0026#34;; // 注解属性，default 表示可选 } // 使用 @MyTest(\u0026#34;这是一个测试方法\u0026#34;) public void testMethod() { ... } 为什么需要注解：注解提供了\u0026quot;在代码旁边放元数据\u0026quot;的能力。没有注解，JUnit 不知道哪些方法是测试；没有 @Override，你以为重写了其实没重写（方法签名不一样），编译器也不告诉你。注解把隐式的约定变成了显式的声明。\n7. 反射 7.1 什么是反射 正常情况下，你写 obj.getName()——你知道类型是什么，编译器也知道。反射是反过来：运行时拿到一个未知的对象，动态查看它的类结构，动态调用它的方法。\n// 正常方式：编译时就知道类型 Student s = new Student(); s.study(); // 编译器知道 Student 有 study 方法 // 反射方式：运行时才知道类型 Object obj = ...; // 不知道具体是什么类 Class\u0026lt;?\u0026gt; clazz = obj.getClass(); // 运行时获取类的信息 Method method = clazz.getMethod(\u0026#34;study\u0026#34;); // 按名字获取方法 method.invoke(obj); // 动态调用 7.2 基本用法 Class\u0026lt;?\u0026gt; clazz = Student.class; // 方式 1：类名.class Class\u0026lt;?\u0026gt; clazz2 = obj.getClass(); // 方式 2：对象.getClass() Class\u0026lt;?\u0026gt; clazz3 = Class.forName(\u0026#34;com.example.Student\u0026#34;); // 方式 3：全限定名 // 获取类信息 clazz.getName(); // 类全名 clazz.getSimpleName(); // 类简称 clazz.getDeclaredFields(); // 所有属性 clazz.getDeclaredMethods(); // 所有方法 // 动态创建对象 Object obj = clazz.getDeclaredConstructor().newInstance(); // 动态调用方法 Method method = clazz.getMethod(\u0026#34;setName\u0026#34;, String.class); method.invoke(obj, \u0026#34;Zhang San\u0026#34;); // 动态访问私有字段（连 private 都拦不住） Field field = clazz.getDeclaredField(\u0026#34;name\u0026#34;); field.setAccessible(true); // 暴力破解 private String name = (String) field.get(obj); 7.3 为什么需要反射 你写的代码能调用你写的类，这不需要反射。但框架不知道你会写什么类：\nJUnit 不知道你写了哪些测试方法 —— 靠反射扫描所有 @Test 方法 Spring 不知道你写了哪些 Bean —— 靠反射扫描 @Component 并创建实例 Gson 不知道你的 User 类有什么字段 —— 靠反射把 JSON 字段映射到 Java 对象 Android 你在 XML 里写的 android:onClick=\u0026quot;doSomething\u0026quot; —— 运行时靠反射找到 doSomething 方法 反射是框架的基石。没有反射，框架就需要你在配置文件中手动列出每个类——那是 Java 远古时代的做法。\n7.4 反射的代价 性能低：反射调用比直接调用慢几十倍（需要安全检查、包装参数） 破坏封装：setAccessible(true) 可以绕过 private 编译期安全丢失：方法名拼错了，编译期不报错，运行时才崩 使用原则：业务代码不要用反射。写框架/库/工具类时才用它。日常开发中你更多的是\u0026quot;受益于反射\u0026quot;而不是\u0026quot;使用反射\u0026quot;。\n关键记忆点 异常 try-catch 把正常逻辑和错误处理分开；finally 一定会执行（除了 System.exit） 编译期异常（IOException）必须处理，运行期异常（NPE）不强制 try-with-resources 自动关流，替代 finally 中手动 close IO 流 字节流（InputStream/OutputStream）处理二进制，字符流（Reader/Writer）处理文本 BufferedReader/Writer 是缓冲包装——大幅减少磁盘 IO 次数 多线程 创建线程用 Runnable 而非 Thread——灵活、可复用、能被线程池管理 start() 启动新线程，run() 只是同步调用——不要搞混 多线程操作共享变量 → 竞态条件 → synchronized 保证原子性 线程池避免频繁创建销毁线程，控制并发数——Executors.newFixedThreadPool(n) Lambda \u0026amp; Stream Lambda 只能在函数式接口（单抽象方法）上使用，本质是匿名内部类的语法糖 Stream 中间操作是懒执行的，终端操作才触发计算 注解 \u0026amp; 反射 注解 = 给代码贴标签；反射 = 运行时读取这些标签并动态执行 反射是框架的基石（JUnit、Spring、Gson 都靠它），业务代码不要直接用 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/1.java%E5%9F%BA%E7%A1%80/04-%E8%BF%9B%E9%98%B6%E7%89%B9%E6%80%A7/","summary":"\u003ch1 id=\"java-进阶特性\"\u003eJava 进阶特性\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 2.4 Java 进阶特性\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 异常处理（try-catch-finally, throws, 自定义异常）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e IO 流（File, InputStream, OutputStream, Reader, Writer）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 多线程（Thread, Runnable, 线程池 ExecutorService）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e Lambda 表达式（Java 8+）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e Stream API（Java 8+）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 注解（Annotation）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 反射（Reflection）\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-异常处理\"\u003e1. 异常处理\u003c/h2\u003e\n\u003ch3 id=\"11-为什么需要异常\"\u003e1.1 为什么需要异常\u003c/h3\u003e\n\u003cp\u003e没有异常机制的语言（比如 C）靠返回值来判断错误：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e result \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edoSomething\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (result \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e)  { \u003cspan style=\"color:#75715e\"\u003e/* 错误 1 */\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (result \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e)  { \u003cspan style=\"color:#75715e\"\u003e/* 错误 2 */\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 正常逻辑和错误处理混在一起，层层 if 嵌套，可读性极差\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJava 的异常机制把\u003cstrong\u003e正常逻辑\u003c/strong\u003e和\u003cstrong\u003e错误处理\u003c/strong\u003e分开——代码沿着\u0026quot;快乐路径\u0026quot;写，错误交给 catch 块处理。\u003c/p\u003e\n\u003ch3 id=\"12-异常体系\"\u003e1.2 异常体系\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eThrowable\n├── Error             严重错误，程序无法处理（OutOfMemoryError、StackOverflowError）\n└── Exception         可处理的异常\n    ├── RuntimeException（运行期异常）  不强制处理，如 NullPointerException、ArrayIndexOutOfBoundsException\n    └── 其他（编译期异常）               必须显式处理，如 IOException、SQLException\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003e编译期异常 vs 运行期异常\u003c/strong\u003e：\u003c/p\u003e","title":"04-Java进阶特性"},{"content":"Java 核心 API 参考指导书 2.3 Java 核心 API\n学习清单 String 与 StringBuilder 包装类（Integer, Long, Double 等） Math 类 日期与时间（Date, Calendar, LocalDateTime） 集合框架 List（ArrayList, LinkedList） Set（HashSet, TreeSet） Map（HashMap, TreeMap） 泛型（Generics） 1. String 与 StringBuilder 1.1 String 的不可变性 String 是 Java 中最特殊的类之一：一旦创建，内容永远不能改。\nString s = \u0026#34;Hello\u0026#34;; s = s + \u0026#34; World\u0026#34;; // 看起来像是改了 s，实际是创建了一个新对象 \u0026quot;Hello\u0026quot; 那个对象本身没变——s + \u0026quot; World\u0026quot; 在内存中创建了一个全新的 \u0026quot;Hello World\u0026quot; 对象，然后把 s 指向了它。原来的 \u0026quot;Hello\u0026quot; 如果没被引用，就等着被垃圾回收。\n为什么要把 String 设计成不可变？\n安全：String 无处不在——文件名、URL、数据库密码、类名。如果 String 可变，你在传参的过程中别人偷偷改了内容，后果不可控 字符串常量池：因为不可变，相同内容的字符串才能共享同一份内存（见下面） HashMap 的 key：String 是最常用的 key。如果 String 可变，hashCode 就会变，存进去的 value 就找不回来了 线程安全：不可变意味着天然线程安全，不需要加锁 1.2 字符串常量池 String s1 = \u0026#34;Hello\u0026#34;; // 字面量 → 放入常量池 String s2 = \u0026#34;Hello\u0026#34;; // 池中已有，直接复用 String s3 = new String(\u0026#34;Hello\u0026#34;); // new → 强制在堆上创建新对象 System.out.println(s1 == s2); // true —— 同一个对象 System.out.println(s1 == s3); // false —— 不同对象 System.out.println(s1.equals(s3)); // true —— 内容相同 == 比较的是内存地址。equals() 比较的是内容。String 重写了 equals()，所以判断字符串内容是否相同用 equals()，不要用 ==。\n1.3 常用方法 String s = \u0026#34; Hello World \u0026#34;; s.length() // 15 s.trim() // \u0026#34;Hello World\u0026#34;（去掉首尾空格） s.toUpperCase() // \u0026#34; HELLO WORLD \u0026#34; s.substring(2, 7) // \u0026#34;Hello\u0026#34;（左闭右开：[2,7)） s.charAt(4) // \u0026#39;l\u0026#39; s.contains(\u0026#34;World\u0026#34;) // true s.indexOf(\u0026#34;o\u0026#34;) // 5（第一个出现的位置） s.replace(\u0026#34;World\u0026#34;, \u0026#34;Java\u0026#34;) // \u0026#34; Hello Java \u0026#34; s.split(\u0026#34; \u0026#34;) // [\u0026#34;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Hello\u0026#34;, \u0026#34;World\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;\u0026#34;] 1.4 StringBuilder：为拼接而生 痛点：\n// 这个循环每一次 += 都会创建一个新的 String 对象 String result = \u0026#34;\u0026#34;; for (int i = 0; i \u0026lt; 10000; i++) { result += i; // 创建了 10000 个临时对象！浪费内存 + 耗时 } 每次 += 都要：分配新内存 → 拷贝旧内容 → 拼接新内容 → 丢弃旧对象。循环 10000 次就产生 10000 个垃圾对象。\nStringBuilder 解决：\nStringBuilder sb = new StringBuilder(); for (int i = 0; i \u0026lt; 10000; i++) { sb.append(i); // 在同一个对象上追加，不创建新的 } String result = sb.toString(); // 最后一次性转成 String StringBuilder 内部是一个可扩容的字符数组，append() 直接在数组尾部写入，不需要反复 new 对象。\nString StringBuilder 可变性 不可变 可变 线程安全 天然安全 不安全（用 StringBuffer 替代） 拼接性能 每次创建新对象 原地追加，快得多 适用场景 内容不变的字符串 频繁拼接/修改的字符串 Java 编译时，简单的 \u0026quot;a\u0026quot; + \u0026quot;b\u0026quot; + \u0026quot;c\u0026quot; 会被自动优化为 StringBuilder，但循环中的 += 不会——编译器没那么聪明。\n2. 包装类 2.1 为什么需要包装类 Java 分两种类型：基本类型（int、double 等）和引用类型（类、接口）。但有些场景只能用引用类型：\n// 泛型不能接受基本类型 List\u0026lt;int\u0026gt; list; // 编译错误！ List\u0026lt;Integer\u0026gt; list; // 正确 // null 只能赋给引用类型 int x = null; // 编译错误！ Integer x = null; // 正确（表示\u0026#34;没有值\u0026#34;） 包装类就是给每个基本类型套了一个\u0026quot;壳\u0026quot;，让它们能以对象的形式存在。\n基本类型 包装类 byte Byte short Short int Integer long Long float Float double Double char Character boolean Boolean 2.2 自动装箱与拆箱 JDK 5 之后，基本类型和包装类之间可以自动转换：\nInteger i = 100; // 自动装箱：int → Integer（编译器插入 Integer.valueOf(100)） int j = i; // 自动拆箱：Integer → int（编译器插入 i.intValue()） // 运算时也会自动拆箱 Integer a = 10; Integer b = 20; Integer c = a + b; // a、b 先拆箱成 int，计算后结果再装箱成 Integer 2.3 常用方法 // 字符串 → 数值 int x = Integer.parseInt(\u0026#34;123\u0026#34;); // 123 double d = Double.parseDouble(\u0026#34;3.14\u0026#34;); // 3.14 // 数值 → 字符串 String s = Integer.toString(100); // \u0026#34;100\u0026#34; String s2 = String.valueOf(100); // \u0026#34;100\u0026#34;（更通用） // 常量 Integer.MAX_VALUE // 2147483647 Integer.MIN_VALUE // -2147483648 注意：Integer.parseInt(\u0026quot;abc\u0026quot;) 会抛出 NumberFormatException。\n3. Math 类 纯工具类，全部是静态方法，不需要创建对象。\nMath.abs(-5) // 5 绝对值 Math.max(3, 8) // 8 最大值 Math.min(3, 8) // 3 最小值 Math.sqrt(16) // 4.0 平方根 Math.pow(2, 3) // 8.0 次方 Math.random() // 0.0~1.0 随机数（不含 1） Math.round(3.6) // 4 四舍五入 Math.floor(3.6) // 3.0 向下取整 Math.ceil(3.2) // 4.0 向上取整 Math.PI // 3.14159... 圆周率常量 4. 日期与时间 4.1 旧 API 的问题 Java 最早的日期类是 java.util.Date，但它设计得很糟糕：\nDate now = new Date(); System.out.println(now); // Tue May 26 10:30:00 CST 2026 —— 格式不可控 System.out.println(now.getYear()); // 返回 126！（从 1900 年算起，1900+126=2026） System.out.println(now.getMonth()); // 返回 4！（0 代表 1 月，4 代表 5 月，反人类） 三大痛点：\n问题 说明 月从 0 开始 1 月是 0，12 月是 11——无数次 bug 的根源 可变对象 date.setMonth(6) 会直接修改对象——线程不安全 API 混乱 Date 只负责时间戳，格式化要靠 SimpleDateFormat，计算要靠 Calendar——三个类各管各的 后来 Java 1.1 推出了 Calendar：\nCalendar cal = Calendar.getInstance(); cal.set(2026, Calendar.MAY, 26); // 还是 0-based 的月份！ int year = cal.get(Calendar.YEAR); // get/set 方法冗长 // 计算 10 天后的日期 cal.add(Calendar.DAY_OF_MONTH, 10); // 直接修改了原对象！不是返回新对象 Calendar 仍然是可变的、月份仍从 0 开始、API 依旧啰嗦。\n4.2 Java 8 新 API：LocalDateTime Java 8（2014 年）引入了全新的 java.time 包，参考了业界优秀的 Joda-Time 库：\n// 获取当前时间 LocalDate today = LocalDate.now(); // 2026-05-26 LocalTime now = LocalTime.now(); // 10:30:00.123 LocalDateTime dt = LocalDateTime.now(); // 2026-05-26T10:30:00.123 // 创建指定时间 LocalDate date = LocalDate.of(2026, 5, 26); // 月份是正常的 1~12！ LocalDateTime dt2 = LocalDateTime.of(2026, 5, 26, 10, 30); // 操作时间——返回新对象，原对象不变（不可变！） LocalDate nextWeek = today.plusDays(7); LocalDate lastMonth = today.minusMonths(1); int year = today.getYear(); // 2026 int month = today.getMonthValue(); // 5（不是 4！） // 格式化 DateTimeFormatter fmt = DateTimeFormatter.ofPattern(\u0026#34;yyyy/MM/dd HH:mm:ss\u0026#34;); String str = dt.format(fmt); // \u0026#34;2026/05/26 10:30:00\u0026#34; LocalDateTime parsed = LocalDateTime.parse(\u0026#34;2026/05/26 10:30:00\u0026#34;, fmt); 4.3 新旧对比总结 旧 API (Date/Calendar) 新 API (java.time) 可变性 可变，线程不安全 不可变，线程安全 月份 0~11，反直觉 1~12，正常 API 设计 分散在多类中 统一在 java.time 包下 格式化 SimpleDateFormat（线程不安全！） DateTimeFormatter（线程安全） 计算 cal.add() 修改原对象 plusXxx() 返回新对象 原则：写新代码永远用 java.time 包。学旧 API 只是为了能读懂老项目的代码。\n5. 集合框架 5.1 为什么需要集合 数组有两个硬伤：\n// 问题 1：长度固定 String[] arr = new String[3]; arr[0] = \u0026#34;A\u0026#34;; arr[1] = \u0026#34;B\u0026#34;; arr[2] = \u0026#34;C\u0026#34;; // arr[3] = \u0026#34;D\u0026#34;; 越界！数组不能自动扩容 // 问题 2：缺少常用操作 // 数组没有：contains()、indexOf()、remove()、sort()……全要手写 集合就是\u0026quot;增强版数组\u0026quot;——自动扩容、内置增删改查、选择不同的数据结构（List/Set/Map）来匹配不同使用场景。\n5.2 集合家族总览 Collection（接口） ├── List（接口） 有序、可重复、有索引 │ ├── ArrayList 底层数组，查快改慢 │ └── LinkedList 底层双向链表，增删快查慢 │ └── Set（接口） 无序、不可重复 ├── HashSet 基于哈希表，最快 └── TreeSet 基于红黑树，自动排序 Map（接口） 键值对，独立于 Collection ├── HashMap 基于哈希表，key 不可重复 └── TreeMap 基于红黑树，key 自动排序 5.3 ArrayList — 最常用的列表 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // 增 list.add(\u0026#34;Apple\u0026#34;); list.add(\u0026#34;Banana\u0026#34;); list.add(1, \u0026#34;Cherry\u0026#34;); // 在索引 1 插入 // 删 list.remove(\u0026#34;Banana\u0026#34;); // 按内容删 list.remove(0); // 按索引删 // 改 list.set(0, \u0026#34;Updated\u0026#34;); // 查 String s = list.get(0); // 按索引取 boolean has = list.contains(\u0026#34;Apple\u0026#34;); int idx = list.indexOf(\u0026#34;Cherry\u0026#34;); int size = list.size(); // 遍历 for (String item : list) { ... } 5.4 ArrayList vs LinkedList ArrayList LinkedList 底层 数组 双向链表 按索引访问 get(i) 快 O(1) 慢 O(n)（要从头遍历） 头部插入/删除 慢 O(n)（元素需搬家） 快 O(1) 尾部追加 快（均摊 O(1)） 快 O(1) 内存 紧凑 每个节点多存两个指针 选择原则：90% 的场景用 ArrayList。除非你的代码频繁在列表头部插入/删除（比如实现一个队列），才考虑 LinkedList。\n5.5 HashSet — 自动去重的集合 Set\u0026lt;String\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); set.add(\u0026#34;A\u0026#34;); set.add(\u0026#34;B\u0026#34;); set.add(\u0026#34;A\u0026#34;); // 重复的 \u0026#34;A\u0026#34; 不会存进去 System.out.println(set); // [A, B]（顺序不保证） System.out.println(set.size()); // 2 // 判断是否存在（最快：O(1)） if (set.contains(\u0026#34;A\u0026#34;)) { ... } // 遍历（顺序不确定） for (String s : set) { ... } HashSet 的去重原理：\n先比较 hashCode()——不同直接判定不同 hashCode 相同再比较 equals()——确认是否真的相同 所以放入 HashSet 的对象，必须正确重写 hashCode() 和 equals()。String、Integer 等 JDK 自带类已经重写好了，直接用。 5.6 TreeSet — 自动排序的去重集合 Set\u0026lt;Integer\u0026gt; set = new TreeSet\u0026lt;\u0026gt;(); set.add(5); set.add(1); set.add(3); set.add(2); System.out.println(set); // [1, 2, 3, 5] —— 自动升序 Set\u0026lt;String\u0026gt; words = new TreeSet\u0026lt;\u0026gt;(); words.add(\u0026#34;banana\u0026#34;); words.add(\u0026#34;apple\u0026#34;); words.add(\u0026#34;cherry\u0026#34;); System.out.println(words); // [apple, banana, cherry] —— 字典序 TreeSet 基于红黑树，插入删除 O(log n)，每次操作后自动保持有序。\n5.7 HashMap — 最常用的键值对 Map\u0026lt;String, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 增/改 map.put(\u0026#34;Zhang San\u0026#34;, 90); map.put(\u0026#34;Li Si\u0026#34;, 85); // 查 int score = map.get(\u0026#34;Li Si\u0026#34;); // 85 boolean has = map.containsKey(\u0026#34;Zhang San\u0026#34;); // 删 map.remove(\u0026#34;Li Si\u0026#34;); // 遍历（三种方式） for (Map.Entry\u0026lt;String, Integer\u0026gt; entry : map.entrySet()) { System.out.println(entry.getKey() + \u0026#34; -\u0026gt; \u0026#34; + entry.getValue()); } for (String key : map.keySet()) { ... } // 只遍历 key for (Integer value : map.values()) { ... } // 只遍历 value HashMap 的 key 也是靠 hashCode() + equals() 来判断是否重复，跟 HashSet 一样的规则。\n6. 泛型 6.1 没有泛型时的噩梦 Java 1.4 及以前，集合是这样用的：\n// 没有泛型：集合里存什么全靠自觉 List list = new ArrayList(); list.add(\u0026#34;Hello\u0026#34;); list.add(123); // 什么都往里塞——编译器不拦 // 取出时必须强制转型 String s = (String) list.get(0); // 每次都要转型 // 直到运行时才发现问题 String s2 = (String) list.get(1); // Integer 不能转 String → ClassCastException 崩了 问题：类型错误被推迟到运行时才发现。编译期完全不管，写错了也能编译通过，到了线上才爆炸。\n6.2 泛型解决：编译期把关 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // 声明：这个 list 只能放 String list.add(\u0026#34;Hello\u0026#34;); // list.add(123); // 编译直接报错！ String s = list.get(0); // 取出来就是 String，不需要转型 效果：把类型检查从运行时提前到编译期——bug 发现越早，修复成本越低。\n6.3 泛型类 // 定义一个可以装任意类型数据的箱子 class Box\u0026lt;T\u0026gt; { // T = Type，占位符，调用时确定具体类型 private T data; public void set(T data) { this.data = data; } public T get() { return data; } } Box\u0026lt;String\u0026gt; strBox = new Box\u0026lt;\u0026gt;(); strBox.set(\u0026#34;Hello\u0026#34;); String s = strBox.get(); // 取出来是 String，不用转型 Box\u0026lt;Integer\u0026gt; intBox = new Box\u0026lt;\u0026gt;(); intBox.set(100); int i = intBox.get(); // 取出来是 Integer，自动拆箱 6.4 泛型方法 一个方法需要处理多种类型的数组，不写泛型就得为每种类型各写一个：\n// 泛型方法：一个方法通吃所有类型 public static \u0026lt;T\u0026gt; void printArray(T[] arr) { for (T item : arr) { System.out.print(item + \u0026#34; \u0026#34;); } System.out.println(); } String[] words = {\u0026#34;Hello\u0026#34;, \u0026#34;World\u0026#34;}; Integer[] nums = {1, 2, 3}; printArray(words); // 自动推断 T = String printArray(nums); // 自动推断 T = Integer 6.5 菱形语法 // JDK 7 之前：右边也要写全泛型类型 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); // 啰嗦 // JDK 7+：右边可以省略，编译器自动推断 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // 简洁 6.6 泛型的核心价值 类型安全：错误从运行时提前到编译期 消除强制转型：取出时直接就是正确的类型 代码复用：一个 Box\u0026lt;T\u0026gt; 替代 N 个 BoxString、BoxInteger\u0026hellip; 关键记忆点 String String 不可变——所有操作返回新对象，原对象不变 == 比地址，equals() 比内容——字符串判等用 equals() 循环中频繁拼接用 StringBuilder，不要用 += 包装类 基本类型不能用于泛型，不能赋 null——包装类解决这两个问题 自动装箱 Integer i = 100 本质是 Integer.valueOf(100) 日期时间 写新代码永远用 java.time 包（LocalDate / LocalDateTime），旧 API 只是为了读老项目 旧 API 的月份从 0 开始（0 = 1 月），新 API 从 1 开始 集合框架 ArrayList 查快改慢（数组），LinkedList 增删快查慢（链表）——默认选 ArrayList HashSet 去重靠 hashCode() + equals()——放入的对象要正确重写这两个方法 HashMap 的 key 同样靠 hashCode() + equals() 判重 泛型 泛型把类型检查从运行时提前到编译期，消除 ClassCastException Box\u0026lt;T\u0026gt; 是\u0026quot;类型参数化\u0026quot;——一个类服务所有类型，不用每种类型写一个版本 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/1.java%E5%9F%BA%E7%A1%80/03-%E6%A0%B8%E5%BF%83api/","summary":"\u003ch1 id=\"java-核心-api\"\u003eJava 核心 API\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 2.3 Java 核心 API\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e String 与 StringBuilder\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 包装类（Integer, Long, Double 等）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e Math 类\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 日期与时间（Date, Calendar, LocalDateTime）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 集合框架\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e List（ArrayList, LinkedList）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e Set（HashSet, TreeSet）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e Map（HashMap, TreeMap）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 泛型（Generics）\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-string-与-stringbuilder\"\u003e1. String 与 StringBuilder\u003c/h2\u003e\n\u003ch3 id=\"11-string-的不可变性\"\u003e1.1 String 的不可变性\u003c/h3\u003e\n\u003cp\u003eString 是 Java 中最特殊的类之一：\u003cstrong\u003e一旦创建，内容永远不能改\u003c/strong\u003e。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString s \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Hello\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003es \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e s \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34; World\u0026#34;\u003c/span\u003e;    \u003cspan style=\"color:#75715e\"\u003e// 看起来像是改了 s，实际是创建了一个新对象\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e\u0026quot;Hello\u0026quot;\u003c/code\u003e 那个对象本身没变——\u003ccode\u003es + \u0026quot; World\u0026quot;\u003c/code\u003e 在内存中创建了一个全新的 \u003ccode\u003e\u0026quot;Hello World\u0026quot;\u003c/code\u003e 对象，然后把 \u003ccode\u003es\u003c/code\u003e 指向了它。原来的 \u003ccode\u003e\u0026quot;Hello\u0026quot;\u003c/code\u003e 如果没被引用，就等着被垃圾回收。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e为什么要把 String 设计成不可变？\u003c/strong\u003e\u003c/p\u003e","title":"03-Java核心API"},{"content":"Java 面向对象编程 参考指导书 2.2 面向对象编程（OOP）\n学习清单 类与对象 构造方法 封装（private, default, protected, public） 继承（extends） 多态（方法重载、方法重写） 抽象类（abstract class） 接口（interface） 内部类（成员内部类、静态内部类、匿名内部类、局部内部类） 枚举（enum） 1. 类与对象 核心概念 类（Class）：对象的模板/蓝图，定义了属性和行为 对象（Object）：类的具体实例，通过 new 关键字创建 属性（Field）：类中的变量，描述对象的状态 方法（Method）：类中的函数，描述对象的行为 最小示例 public class Person { // 属性（成员变量） String name; int age; // 方法 void introduce() { System.out.println(\u0026#34;I\u0026#39;m \u0026#34; + name + \u0026#34;, \u0026#34; + age + \u0026#34; years old.\u0026#34;); } } // 使用 Person p = new Person(); // 创建对象 p.name = \u0026#34;Zhang San\u0026#34;; // 给属性赋值 p.age = 20; p.introduce(); // 调用方法 内存模型 Person p = new Person(); ↑ ↑ 栈上的引用 堆上的对象（实际数据） p 是栈上的一个引用变量（4/8 字节），指向堆中真正的 Person 对象。new 干了三件事：堆上分配内存 → 初始化默认值 → 返回引用地址。\n2. 构造方法 什么是构造方法 方法名与类名完全相同 没有返回值类型（连 void 都不写） 通过 new 调用，在对象创建时自动执行 如果没写，编译器会自动生成无参构造（默认构造） 构造方法重载 public class Person { String name; int age; // 无参构造 public Person() { this(\u0026#34;unknown\u0026#34;, 0); // 调用下面的有参构造 } // 有参构造 public Person(String name, int age) { this.name = name; // this.name 是属性，name 是参数 this.age = age; } } Person p1 = new Person(); // 调无参 Person p2 = new Person(\u0026#34;Li Si\u0026#34;, 25); // 调有参 this 关键字 this.属性：区分成员变量和局部变量（同名时） this()：在构造方法中调用另一个构造方法（必须写在第一行） 3. 封装 核心思想 把属性设为 private，对外提供 public 的 getter/setter 控制访问，可以在 setter 中做校验。\n四种访问修饰符 修饰符 本类 同包 子类 任意 private ✅ ❌ ❌ ❌ default（不写） ✅ ✅ ❌ ❌ protected ✅ ✅ ✅ ❌ public ✅ ✅ ✅ ✅ 最小示例 public class Person { private String name; // 外部不能直接访问 private int age; // getter —— 读 public String getName() { return name; } public int getAge() { return age; } // setter —— 写，可以加校验 public void setName(String name) { this.name = name; } public void setAge(int age) { if (age \u0026gt;= 0) this.age = age; // 防止非法值 } } 为什么封装？ 直接暴露成员变量 (public String name) 会导致调用方随意修改，无法控制数据有效性。封装让类的内部实现可以自由变化，不影响外部调用方。\n4. 继承 核心概念 用 extends 关键字 子类拥有父类所有非 private 的属性和方法 Java 是单继承：一个类只能继承一个直接父类 所有类的最终祖先都是 Object 子类不会继承父类的构造方法 public class Student extends Person { private String school; public Student(String name, int age, String school) { super(name, age); // 必须调用父类构造，且必须在第一行 this.school = school; } // 子类独有方法 public void study() { System.out.println(getName() + \u0026#34; is studying...\u0026#34;); } } super 关键字 super(参数)：调用父类构造，必须写在子类构造的第一行 super.方法()：调用父类中被重写的方法 子类对象的创建顺序 父类静态代码块 → 子类静态代码块 → 父类构造 → 子类构造 为什么需要继承？—— 消除重复代码 没有继承的世界：假设你有 3 种用户——学生、老师、管理员，每种都要写 name、age、login、logout。\n// 反模式：每个类各自为战，大量重复 class Student { String name; int age; // ← 重复 void login() { /* ... */ } // ← 重复 void logout() { /* ... */ } // ← 重复 void submitHomework() { /* 特有 */ } } class Teacher { String name; int age; // ← 又重复一遍 void login() { /* ... */ } // ← 又重复一遍 void logout() { /* ... */ } // ← 又重复一遍 void gradePaper() { /* 特有 */ } } class Admin { String name; int age; // ← 第三遍 void login() { /* ... */ } // ← 第三遍 void logout() { /* ... */ } // ← 第三遍 void manageSystem() { /* 特有 */ } } 问题：想给 login 加个验证码功能 → 3 个类各改一遍 → 漏改一个就是 bug。\n用继承解决：\nclass User { // 父类：抽取共性 String name; int age; void login() { /* 统一实现 */ } void logout() { /* 统一实现 */ } } class Student extends User { // 子类：只写自己特有的 void submitHomework() { /* ... */ } } class Teacher extends User { void gradePaper() { /* ... */ } } class Admin extends User { void manageSystem() { /* ... */ } } 效果：login 加验证码 → 只改 User 一处 → 所有子类自动生效。\n继承的设计原则：DRY（Don\u0026rsquo;t Repeat Yourself）。当你发现多个类有相同的属性和行为，说明该抽取父类了。反过来，如果两个类只有一两处相似、大部分不同，就不该强行继承——继承不是万能胶。\n5. 多态 两个维度 方向 技术 发生时机 目标 编译时多态 方法重载 同一个类中 方法名相同，参数列表不同 运行时多态 方法重写 父子类之间 子类覆盖父类同名方法 方法重载（Overload） public class Calculator { public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } // 参数类型不同 public int add(int a, int b, int c) { return a + b + c; } // 参数个数不同 } 重载规则：方法名相同，参数列表必须不同（类型/个数/顺序），跟返回值无关。\n方法重写（Override） public class Student extends Person { @Override // 注解：编译器帮你检查是否真的重写了 public void introduce() { // 子类重新实现父类方法 super.introduce(); // 也可以先调父类 System.out.println(\u0026#34;I study at \u0026#34; + school); } } 重写规则：\n方法签名（方法名 + 参数列表）必须完全相同 返回值类型可以是被重写方法返回值的子类型（协变返回） 访问权限不能比父类更严格（父 public → 子 public，不能变 private） static 方法不能被重写（只能隐藏） final 方法不能被重写 多态的核心用法 Animal dog = new Dog(); // 父类引用 指向 子类对象 dog.makeSound(); // 实际调用的是 Dog 的方法（运行时决定） // 参数用父类型 → 可以传入任何子类型对象 public void feed(Animal a) { ... } feed(new Dog()); feed(new Cat()); instanceof 与向下转型 if (animal instanceof Dog) { Dog dog = (Dog) animal; // 向下转型，调用 Dog 独有的方法 dog.play(); } 为什么需要多态？—— 写出通用代码 核心问题：如何让一段代码处理不同类型，而不需要为每种类型写一个版本？\n没有多态的世界：\n// 喂动物的方法 —— 每多一种动物，就要多写一个方法 public void feedDog(Dog dog) { dog.eat(); } public void feedCat(Cat cat) { cat.eat(); } public void feedBird(Bird bird) { bird.eat(); } // 调用方必须知道具体类型 if (type == \u0026#34;Dog\u0026#34;) feedDog(dog); else if (type == \u0026#34;Cat\u0026#34;) feedCat(cat); else if (type == \u0026#34;Bird\u0026#34;) feedBird(bird); 问题：加一个新动物（Fish）→ 新增 feedFish 方法 → 调用方加 else if → 每加一个动物要改两处。真正的噩梦是这样的代码散落在项目各处，改漏一处就是运行时 bug。\n用多态解决：\n// 父类：普通动物类 class Animal { public void eat() { System.out.println(\u0026#34;动物在吃东西\u0026#34;); } } // 子类：狗 class Dog extends Animal { @Override public void eat() { System.out.println(\u0026#34;狗正在啃骨头\u0026#34;); } } // 子类：猫 class Cat extends Animal { @Override public void eat() { System.out.println(\u0026#34;猫正在吃鱼\u0026#34;); } } // 饲养员类 class Feeder { public void feed(Animal a) { a.eat(); // 运行时动态绑定，调用实际对象的方法 } } 为什么叫\u0026quot;多态\u0026quot;：a.eat() 这一行代码，根据 a 实际指向的对象类型表现出不同行为——同一个方法调用，多种形态。这个决策不是编译期写死的，而是运行时 JVM 根据实际对象类型动态分派的（动态绑定 / 虚方法调用）。\n多态的设计原则：开闭原则（Open-Closed Principle）——对扩展开放，对修改关闭。加新功能是\u0026quot;新增代码\u0026quot;而不是\u0026quot;改旧代码\u0026quot;。改旧代码有风险，可能引入 bug；加新代码是安全的。\n多态的实现前提 继承/接口：必须有 is-a 关系 方法重写：子类重写父类方法 父类引用指向子类对象：Animal a = new Dog(); 三者缺一不可。只写 Dog d = new Dog() 然后用 d.makeSound()，这不叫多态，叫普通方法调用——你根本没有利用\u0026quot;用父类型处理子类型\u0026quot;的灵活性。\n6. 抽象类 核心概念 用 abstract 修饰 不能实例化（new Animal() 编译报错） 可以有抽象方法（没有方法体），也可以有普通方法 子类必须实现所有抽象方法，否则自己也要声明为抽象类 // 定义抽象动物类，声明统一的抽象方法 abstract class Animal { public abstract void eat(); } // 狗类 class Dog extends Animal { @Override public void eat() { System.out.println(\u0026#34;狗正在啃骨头\u0026#34;); } } // 猫类 class Cat extends Animal { @Override public void eat() { System.out.println(\u0026#34;猫正在吃鱼\u0026#34;); } } // 饲养员类 class Feeder { // 一个方法通吃所有动物，体现多态 public void feed(Animal a) { a.eat(); // 运行时自动调用对应的 eat() } } // 测试类 public class AnimalTest { public static void main(String[] args) { Feeder feeder = new Feeder(); // 传什么具体的动物，就执行什么动物具体的吃法 feeder.feed(new Dog()); feeder.feed(new Cat()); // 新增 Cat 时，饲养员调用方零改动 } } 抽象类 vs 普通类 普通类 抽象类 实例化 可以 不可以 抽象方法 不能有 可以有 构造方法 可以有 可以有（给子类用） 意义 具体事物 抽取子类共性 为什么需要抽象类？—— 强制子类遵守契约 问题：普通父类的方法给了默认实现，但有些场景下父类自己根本不知道该怎么实现。\n// 普通父类：makeSound() 写什么？ class Animal { void makeSound() { // ??? 每个动物叫声都不一样，这里写什么都不对 System.out.println(\u0026#34;???\u0026#34;); } } 如果你在父类写一个空方法体 { }，子类忘了重写，程序静默执行空方法——不会有任何错误提示，但功能缺失。这种 bug 极难发现（它不崩，只是不干活）。\n抽向类解决：\nabstract class Animal { public abstract void makeSound(); // 不写方法体 → 语法上强制子类实现 } class Dog extends Animal { // 如果不写 makeSound()，编译直接报错！ @Override public void makeSound() { System.out.println(\u0026#34;Woof!\u0026#34;); } } 效果：子类忘记实现 → 编译期就报错，不可能带到运行时。这就是\u0026quot;把错误左移\u0026quot;——bug 发现得越早，修复成本越低（编译期 \u0026lt; 测试阶段 \u0026lt; 线上事故）。\n抽象类的另一个经典用法：模板方法模式 父类定义一个算法的骨架，把可变步骤交给子类实现：\nabstract class DataExporter { // 模板方法（final：不让子类改流程） public final void export() { connect(); writeData(); close(); } private void connect() { System.out.println(\u0026#34;Connected\u0026#34;); } // 固定步骤 private void close() { System.out.println(\u0026#34;Closed\u0026#34;); } // 固定步骤 abstract void writeData(); // 可变步骤 → 子类实现 } class PdfExporter extends DataExporter { void writeData() { System.out.println(\u0026#34;Writing PDF...\u0026#34;); } } class ExcelExporter extends DataExporter { void writeData() { System.out.println(\u0026#34;Writing Excel...\u0026#34;); } } 调用方只需要 exporter.export()，不用关心具体格式。新增导出格式只需写一个新子类——流程固定，细节可变。这是 Android 中 BaseActivity 的常见模式。\n7. 接口 核心概念 用 interface 声明，用 implements 实现 定义行为的契约：实现类必须提供所有声明的方法 一个类可以实现多个接口（打破单继承限制） 接口中所有方法默认是 public abstract Java 8+ 可以有 default 方法（有默认实现） interface Pet { void play(); // 默认 public abstract default void cuddle() { // Java 8+: 默认方法，可选重写 System.out.println(\u0026#34;Being cuddled...\u0026#34;); } } class Dog extends Animal implements Pet { public Dog(String name) { super(name); } @Override public void makeSound() { System.out.println(name + \u0026#34;: Woof!\u0026#34;); } @Override public void play() { // 必须实现 Pet 的 play System.out.println(name + \u0026#34; plays fetch.\u0026#34;); } } 接口 vs 抽象类 抽象类 接口 关键字 abstract class interface 关系 extends（单继承） implements（多实现） 成员变量 可以有任意变量 只能有常量（public static final） 构造方法 有 没有 方法 抽象方法 + 普通方法 抽象方法 + default 方法（Java 8+） 设计意图 \u0026ldquo;is-a\u0026rdquo;（是什么） \u0026ldquo;can-do\u0026rdquo;（能做什么） Android 场景：View.OnClickListener 就是接口——任何类实现了它就能响应点击。\n为什么需要接口？—— 三层递进理解 接口有两个作用，我们由浅入深来讲。\n第一层：避免单继承的限制（最直观） Java 规定一个类只能有一个亲爹（单继承）。但现实中的东西往往有多个身份：\n// 狗既是一种动物，又可以当宠物 // 但 extends 只能写一个！ class Dog extends Animal { } // 只能继承一个 接口就是\u0026quot;干爹\u0026quot;——你可以认无数个：\nclass Dog extends Animal implements Pet, Serializable, Comparable { // 亲爹 ↑ 干爹1 干爹2 干爹3 } 继承回答\u0026quot;我是什么\u0026quot;，接口回答\u0026quot;我能干什么\u0026quot;。\n第二层：统一不同类的行为（多态的基础） 想象你有一堆完全不相关的设备，但它们都有一个共同的操作：开机。\n// 这三个类没有任何继承关系 class Phone { void powerOn() { System.out.println(\u0026#34;Phone booting...\u0026#34;); } } class TV { void powerOn() { System.out.println(\u0026#34;TV turning on...\u0026#34;); } } class Computer { void powerOn() { System.out.println(\u0026#34;Computer starting...\u0026#34;); } } 现在你想写一个\u0026quot;一键开机所有设备\u0026quot;的功能。没有接口，你只能这样：\n// 每加一种设备就要加一行 —— 烦！ phone.powerOn(); tv.powerOn(); computer.powerOn(); 接口登场——给它们贴一个共同的\u0026quot;标签\u0026quot;：\n// 第一步：定义接口（贴标签） interface Powerable { void powerOn(); // 有这个标签的，必须会 powerOn } // 第二步：各设备贴上标签 class Phone implements Powerable { public void powerOn() { System.out.println(\u0026#34;Phone booting...\u0026#34;); } } class TV implements Powerable { public void powerOn() { System.out.println(\u0026#34;TV turning on...\u0026#34;); } } class Computer implements Powerable { public void powerOn() { System.out.println(\u0026#34;Computer starting...\u0026#34;); } } // 第三步：一段代码处理所有设备 Powerable[] devices = { new Phone(), new TV(), new Computer() }; for (Powerable d : devices) { d.powerOn(); // 不关心具体是什么设备，只管\u0026#34;能开机\u0026#34;这件事 } 接口在这里的本质：给互不相关的类贴一个共同的标签，让它们可以被同一段代码处理。这跟多态是一样的思想——Powerable d = new Phone()，运行时自动调用 Phone 的 powerOn。\n生活类比：遥控器上的\u0026quot;开关\u0026quot;按钮。遥控器不需要知道你是什么牌子的电视——只要你的电视\u0026quot;实现了\u0026quot;被遥控的接口，同一个按钮就能关掉你。\n第三层：换零件不改代码（解耦） 这是接口真正强大的地方，也是最容易被初学者忽略的地方。\n生活场景：你的手机没电了。\n有接口的世界（现实）： 没有接口的世界（假设）： ┌──────────┐ ┌──────────┐ │ 手机 │ │ 手机 │ │ 充电口是 │── 随便插哪个充电器 │ 只能插 │── 只能用 X 牌充电器 │ USB-C 接口│ │ X 牌充电器 │ └──────────┘ └──────────┘ USB-C 就是一个接口标准。手机只认\u0026quot;USB-C 接口\u0026quot;，不认具体是哪个牌子的充电器。所以你换充电器不需要换手机，换手机也不需要扔充电器。\n翻译成代码——没有接口的世界：\nclass Phone { private XiaomiCharger charger = new XiaomiCharger(); // 焊死小米充电器 void charge() { charger.supplyPower(); // 只能用小米的 } } 如果充电器坏了，你只能买小米的。想用华为的快充？对不起，Phone 类的代码里写死了 XiaomiCharger，想换就得改 Phone 的源码。\n有接口的世界：\n// 第一步：定义充电接口标准 interface Charger { void supplyPower(); } // 第二步：各品牌充电器实现这个标准 class XiaomiCharger implements Charger { public void supplyPower() { System.out.println(\u0026#34;Xiaomi: 18W charging\u0026#34;); } } class HuaweiCharger implements Charger { public void supplyPower() { System.out.println(\u0026#34;Huawei: 66W fast charge\u0026#34;); } } // 第三步：手机只依赖接口，不依赖具体品牌 class Phone { private Charger charger; // 只要是符合接口标准的充电器就行 public Phone(Charger charger) { // 插什么充电器你说了算 this.charger = charger; } void charge() { charger.supplyPower(); // 管它小米华为，能充电就行 } } // 使用 —— 想用哪个就用哪个 Phone p1 = new Phone(new XiaomiCharger()); Phone p2 = new Phone(new HuaweiCharger()); 关键对比：\n没有接口 有接口 换充电器 改 Phone 源码 不关 Phone 的事 测试 必须插真充电器 可以插一个\u0026quot;假的\u0026quot;模拟充电器 加新品牌 改 Phone 源码 写一个新的 implements Charger 即可 总结：接口让你把\u0026quot;我要什么能力\u0026quot;和\u0026quot;谁来实现这个能力\u0026quot;分开。Phone 说\u0026quot;我要一个能供电的东西\u0026quot;，但不关心谁来供电——这就叫解耦。\nAndroid 中你见到的 View.OnClickListener、RecyclerView.Adapter 都是这个道理——系统不知道你要怎么处理点击、显示什么数据，所以定义接口让你自己实现。\n接口 vs 抽象类：怎么选（来自实践） 需要一个\u0026#34;默认骨架\u0026#34; → 抽象类（可以包含成员变量、构造方法、已实现的方法） 需要多个\u0026#34;能力标签\u0026#34; → 接口（一个类可以实现多个接口） \u0026#34;这两个东西本质上是一种东西\u0026#34; → 抽象类（Animal → Dog, Cat） \u0026#34;这两个东西不是一种东西，但能做同样的事\u0026#34; → 接口（Dog, Robot 都实现了 Playable） 实际开发中，接口的使用频率远高于抽象类。现代 Android 开发（依赖注入、Repository 模式、回调监听）大量使用接口来解耦。\n8. 内部类 概述 定义在另一个类内部的类，一共四种。\n内部类 ├── 成员内部类 普通 inner class ├── 静态内部类 static class ├── 局部内部类 定义在方法里 └── 匿名内部类 用最多的，无类名 8.1 成员内部类 public class Outer { private String msg = \u0026#34;Hello\u0026#34;; class Inner { // 不加 static public void print() { System.out.println(msg); // 可以直接访问外部类私有成员 } } } Outer out = new Outer(); Outer.Inner in = out.new Inner(); // 必须通过外部类对象创建 in.print(); 特点：持有外部类引用，能无限制访问外部类成员。容易造成内存泄漏。\n8.2 静态内部类 public class Outer { private static String msg = \u0026#34;Hello\u0026#34;; static class Inner { // 加了 static public void print() { System.out.println(msg); // 只能访问外部类静态成员 } } } Outer.Inner in = new Outer.Inner(); // 不需要外部类对象 in.print(); 特点：不持有外部类引用，不能直接访问外部类的非静态成员。比成员内部类更安全，常用于 ViewHolder 模式。\n8.3 局部内部类 public class Outer { public void method() { final int localVar = 10; // JDK 8+ 可以省略 final，但本质要求 final class LocalInner { // 定义在方法体内部 public void print() { System.out.println(localVar); // 只能访问 final 局部变量 } } LocalInner li = new LocalInner(); // 只能在方法内使用 li.print(); } } 特点：作用域仅限当前方法，用得很少。\n8.4 匿名内部类 痛点：为了一行代码要单独建一个文件 假设你有一个按钮，点击后弹出一句话。按常规做法，你得：\n// 步骤 1：写一个类实现点击接口 class MyClickListener implements View.OnClickListener { @Override public void onClick(View v) { System.out.println(\u0026#34;按钮被点了！\u0026#34;); } } // 步骤 2：在另一个地方创建对象并设置 button.setOnClickListener(new MyClickListener()); 就为了实现一行 System.out.println(\u0026quot;按钮被点了！\u0026quot;)，你要新建一个类、给它起个名字、写一堆样板代码。如果页面上有 5 个按钮，每个按钮的点击行为不同，你就要建 5 个类——MyClickListener1、MyClickListener2……\n匿名内部类就是为了消灭这种\u0026quot;一次性类\u0026quot;而生的。\n一句话理解 匿名内部类 = 现场创建对象 + 现场写好方法，不给类起名字 就像一次性筷子——当场拆开用，用完就不管了，不需要给它起名字放进碗柜。\n渐进式理解 第一步：先看常规写法（你已知的）\n// 1. 定义一个类 class Dog implements Pet { public void play() { System.out.println(\u0026#34;Dog plays fetch.\u0026#34;); } } // 2. 创建这个类的对象 Pet pet = new Dog(); pet.play(); 第二步：匿名内部类的写法——把上面两步合并成一步\nPet pet = new Pet() { // ← 注意：这里是 new Pet()，不是 new Dog() public void play() { // ← 现场直接把方法写好 System.out.println(\u0026#34;Dog plays fetch.\u0026#34;); } }; // ← 分号！这是一个完整的语句 pet.play(); 关键疑问：new Pet()？接口不是不能 new 吗？\nPet pet = new Pet() { ... }; ↑ ↑ 不是真的在 这个大括号里的内容 new 接口 定义了一个\u0026#34;无名类\u0026#34; new Pet() { ... } 的意思是：创建一个没有名字的类，这个类实现了 Pet 接口，然后立即创建它的对象。语法糖，只是看起来像在 new 接口。\n不是 new 接口，是 new (一个实现了接口的匿名类)。Pet 只是告诉你这个匿名类遵守什么协议。\n第三步：对比一目了然\n常规做法： 匿名类做法： ┌─────────────────────┐ ┌─────────────────────┐ │ 文件 Dog.java: │ │ 一行搞定： │ │ class Dog impl Pet {│ │ Pet p = new Pet() { │ │ void play() {...} │ │ void play() {...} │ │ } │ │ }; │ │ │ └─────────────────────┘ │ 调用处： │ 不建文件 │ Pet p = new Dog(); │ 不起类名 └─────────────────────┘ 用完即弃 需要单独的 .java 文件 完整示例：按钮点击 // 场景：页面上有两个按钮，点击后做不同的事 // 按钮 1：弹出\u0026#34;收藏成功\u0026#34; button1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println(\u0026#34;收藏成功！\u0026#34;); } }); // 按钮 2：弹出\u0026#34;已取消\u0026#34; button2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println(\u0026#34;已取消！\u0026#34;); } }); 不用建两个类文件，行为就写在设置监听的地方——逻辑紧凑、一目了然。\n什么时候用？ 一个接口/抽象类的实现 ──▶ 只用到一次 ──▶ 匿名内部类 ✓ ──▶ 会在多处复用 ──▶ 老老实实建一个普通类 匿名内部类适合\u0026quot;一次性使用\u0026quot;的场景。如果同一个实现要在 3 个地方用到，就别用匿名——老老实实定义一个类，避免代码重复。\nLambda 简化（预告） 上面的按钮代码在 Android Studio 里会被提示简化为一行：\nbutton.setOnClickListener(v -\u0026gt; System.out.println(\u0026#34;收藏成功！\u0026#34;)); 这行代码做的事情跟匿名内部类完全一样，只是语法更短。后面学 Lambda 的时候会详细讲，现在你只需要知道：Lambda 是匿名内部类的\u0026quot;极简版\u0026quot;语法。\n记住本质：new Xxx() { 方法实现 } = 现场创建了一个实现了 Xxx 的无名类对象。编译后确实会生成 Outer$1.class 这样的文件，只是你不用手动管理它。\n9. 枚举 基本用法 enum WeekDay { MON, TUE, WED, THU, FRI, SAT, SUN } WeekDay today = WeekDay.MON; // 类型安全：不会出现非法值 // 遍历 for (WeekDay d : WeekDay.values()) { System.out.println(d); // 输出 MON TUE ... } // switch switch (today) { case MON: System.out.println(\u0026#34;Monday\u0026#34;); break; case TUE: System.out.println(\u0026#34;Tuesday\u0026#34;); break; } 带属性的枚举 enum WeekDay { MON(\u0026#34;周一\u0026#34;), TUE(\u0026#34;周二\u0026#34;), WED(\u0026#34;周三\u0026#34;), THU(\u0026#34;周四\u0026#34;), FRI(\u0026#34;周五\u0026#34;), SAT(\u0026#34;周六\u0026#34;), SUN(\u0026#34;周日\u0026#34;); private String chinese; WeekDay(String ch) { this.chinese = ch; } // 构造方法默认 private public String getChinese() { return chinese; } } WeekDay.MON.getChinese() // \u0026#34;周一\u0026#34; 关键记忆点 基础规则 new 干了三件事：分配内存 → 初始化默认值 → 返回引用 构造方法：无返回值类型、名=类名、this() 调用同类构造必须在第一行 封装：private 属性 + public getter/setter，目的是控制访问 + 方便后续修改 Java 单继承：一个类只能 extends 一个父类，但可以 implements 多个接口 多态核心：父类引用指向子类对象 → 运行时决定调用谁的方法 抽象 vs 接口：abstract class 是 \u0026ldquo;is-a\u0026rdquo;（共性提取）；interface 是 \u0026ldquo;can-do\u0026rdquo;（能力契约） 匿名内部类 编译后产生 $1.class，持有外部类引用时要小心内存泄漏 子类构造必须第一行调 super() 或 this() 设计思维 继承解决的是\u0026quot;代码重复\u0026quot;：DRY 原则，公共逻辑收归父类，改一处全局生效 多态解决的是\u0026quot;分支泛滥\u0026quot;：用 父类引用.方法() 替代 if-else 判断类型，新增子类调用方零改动（开闭原则） 抽象类解决的是\u0026quot;父类不知道怎么写\u0026quot;：不写空方法体糊弄，编译期强制子类实现（错误左移） 接口解决的是\u0026quot;单继承不够用 + 高层不该依赖低层\u0026quot;：能力标签 + 依赖倒置，换实现不改调用方 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/1.java%E5%9F%BA%E7%A1%80/02-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B/","summary":"\u003ch1 id=\"java-面向对象编程\"\u003eJava 面向对象编程\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 2.2 面向对象编程（OOP）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 类与对象\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 构造方法\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 封装（private, default, protected, public）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 继承（extends）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 多态（方法重载、方法重写）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 抽象类（abstract class）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 接口（interface）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 内部类（成员内部类、静态内部类、匿名内部类、局部内部类）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 枚举（enum）\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-类与对象\"\u003e1. 类与对象\u003c/h2\u003e\n\u003ch3 id=\"核心概念\"\u003e核心概念\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e类（Class）\u003c/strong\u003e：对象的模板/蓝图，定义了属性和行为\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e对象（Object）\u003c/strong\u003e：类的具体实例，通过 \u003ccode\u003enew\u003c/code\u003e 关键字创建\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e属性（Field）\u003c/strong\u003e：类中的变量，描述对象的状态\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e方法（Method）\u003c/strong\u003e：类中的函数，描述对象的行为\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"最小示例\"\u003e最小示例\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ePerson\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 属性（成员变量）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    String name;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e age;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 方法\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eintroduce\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        System.\u003cspan style=\"color:#a6e22e\"\u003eout\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eprintln\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;I\u0026#39;m \u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e name \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;, \u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e age \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34; years old.\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 使用\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ePerson p \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Person();    \u003cspan style=\"color:#75715e\"\u003e// 创建对象\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ep.\u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Zhang San\u0026#34;\u003c/span\u003e;       \u003cspan style=\"color:#75715e\"\u003e// 给属性赋值\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ep.\u003cspan style=\"color:#a6e22e\"\u003eage\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e 20;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ep.\u003cspan style=\"color:#a6e22e\"\u003eintroduce\u003c/span\u003e();              \u003cspan style=\"color:#75715e\"\u003e// 调用方法\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"内存模型\"\u003e内存模型\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ePerson p = new Person();\n       ↑         ↑\n    栈上的引用   堆上的对象（实际数据）\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003ccode\u003ep\u003c/code\u003e 是栈上的一个引用变量（4/8 字节），指向堆中真正的 Person 对象。\u003ccode\u003enew\u003c/code\u003e 干了三件事：堆上分配内存 → 初始化默认值 → 返回引用地址。\u003c/p\u003e","title":"02-Java面向对象编程"},{"content":"Java 基础语法 参考指导书 2.1 基础语法\n学习清单 JDK 安装与环境配置 第一个 Java 程序：Hello World 基本数据类型（int, long, float, double, boolean, char, byte, short） 变量与常量 运算符（算术、关系、逻辑、位、三元） 流程控制（if-else, switch, for, while, do-while） 数组（一维数组、二维数组） 1. JDK 安装与环境配置 JDK vs JRE 概念 全称 作用 JDK Java Development Kit 开发者工具包，包含 javac（编译器） + java（运行器） + JRE JRE Java Runtime Environment 运行 Java 程序的环境，只有 java 没有 javac JVM Java Virtual Machine 执行字节码的虚拟机，JDK 和 JRE 都包含 编译与运行流程 .java 源文件 ──javac 编译──▶ .class 字节码 ──java 运行──▶ JVM 执行 环境检查 java -version # 查看 JDK 版本 javac -version # 查看编译器版本 2. 第一个 Java 程序：Hello World（程序基本规则详解） 最小代码 // HelloWorld.java public class HelloWorld { public static void main(String[] args) { System.out.println(\u0026#34;Hello, World!\u0026#34;); } } 逐行解析 public class HelloWorld\npublic：访问修饰符，表示这个类对外公开 class：声明一个类，Java 中所有代码都必须放在类中 HelloWorld：类名，必须与文件名完全一致（大小写敏感） public static void main(String[] args)\npublic：main 方法必须公开，JVM 才能调用 static：静态方法，JVM 不需要创建对象就能直接调用 void：main 方法不返回任何值 main：固定方法名，JVM 固定入口，名字不能改 String[] args：命令行参数数组 System.out.println(\u0026quot;Hello, World!\u0026quot;)\nSystem：JDK 内置类 out：System 类的静态成员，代表标准输出流 println：打印一行并换行 \u0026quot;Hello, World!\u0026quot;：字符串字面量，双引号包裹 编译与运行 javac -encoding UTF-8 HelloWorld.java # 编译生成 HelloWorld.class java HelloWorld # 运行（不带 .class 后缀） 类名与文件名的关系 带 public 的类的名称必须与 .java 文件名完全一致 一个 .java 文件最多只能有一个 public class 可以有多个非 public 类 为什么是 main 方法？ JVM 启动时，固定找 public static void main(String[] args) 作为程序入口。签名必须严格匹配——少一个参数、多一个返回值、不用 static 修饰都会导致 \u0026ldquo;找不到 main 方法\u0026rdquo; 错误。\nprint 三兄弟 System.out.print(\u0026#34;不换行\u0026#34;); System.out.println(\u0026#34;换行\u0026#34;); // ln = line System.out.printf(\u0026#34;格式化: %s %d\u0026#34;, \u0026#34;age\u0026#34;, 18); 3. 基本数据类型 Java 是强类型语言：每个变量必须先声明类型才能使用。\n8 种基本类型（Primitive Types） 类型 大小 范围 示例 byte 1 字节 -128 ~ 127 byte b = 100; short 2 字节 -32768 ~ 32767 short s = 30000; int 4 字节 -2³¹ ~ 2³¹-1（约 ±21 亿） int i = 100; long 8 字节 -2⁶³ ~ 2⁶³-1 long l = 100L; ← 必须加 L float 4 字节 约 7 位有效数字 float f = 3.14f; ← 必须加 f double 8 字节 约 15 位有效数字 double d = 3.14; char 2 字节 0 ~ 65535（Unicode） char c = 'A'; boolean 1 位（JVM 相关） true / false boolean b = true; 变量声明与赋值 int age; // 声明 age = 18; // 赋值 int score = 100; // 声明 + 赋值（初始化） score = 200; // 重新赋值 常量 final double PI = 3.14159; // final 修饰，值不能改 // PI = 3.14; // 编译错误！ 类型转换 // 自动转换（小 → 大）：安全，自动完成 int i = 100; long l = i; // int 自动升级为 long // 强制转换（大 → 小）：可能溢出，需手动指定 long big = 1000L; int small = (int) big; // 必须写 (int)，否则编译报错 // float 转 int：小数部分直接截断 float f = 3.99f; int n = (int) f; // n = 3，不是 4 引用类型 vs 基本类型 // 基本类型：存值本身 int a = 10; int b = a; // b = 10，独立拷贝 // 引用类型：存对象地址（String 是引用类型） String s1 = \u0026#34;Hello\u0026#34;; String s2 = s1; // s2 和 s1 指向同一个 \u0026#34;Hello\u0026#34; 对象 4. 运算符 算术运算符 int a = 10, b = 3; a + b // 13 加 a - b // 7 减 a * b // 30 乘 a / b // 3 除（整数除法，结果仍是整数，小数被丢弃） a % b // 1 取余（取模） 自增自减（注意前置后置区别） int x = 5; int y = x++; // y=5, x=6 后置：先赋值再自增 int z = ++x; // z=7, x=7 前置：先自增再赋值 关系运算符（结果 boolean） a == b // 等于 a != b // 不等于 a \u0026gt; b // 大于 a \u0026lt; b // 小于 a \u0026gt;= b // 大于等于 a \u0026lt;= b // 小于等于 逻辑运算符 boolean x = true, y = false; x \u0026amp;\u0026amp; y // false AND（短路：左边为 false 时不计算右边） x || y // true OR （短路：左边为 true 时不计算右边） !x // false NOT 三元运算符（一行 if-else） int max = (a \u0026gt; b) ? a : b; // 如果 a\u0026gt;b 取 a，否则取 b 5. 流程控制 if-else int score = 85; if (score \u0026gt;= 90) { System.out.println(\u0026#34;A\u0026#34;); } else if (score \u0026gt;= 60) { System.out.println(\u0026#34;B\u0026#34;); } else { System.out.println(\u0026#34;C\u0026#34;); } switch int day = 3; switch (day) { case 1: System.out.println(\u0026#34;Mon\u0026#34;); break; case 2: System.out.println(\u0026#34;Tue\u0026#34;); break; default: System.out.println(\u0026#34;Other\u0026#34;); } // 漏写 break 会 \u0026#34;穿透\u0026#34; 到下一个 case for 循环 for (int i = 0; i \u0026lt; 5; i++) { // 初始化; 条件; 步进 System.out.println(i); // 输出 0 1 2 3 4 } // 增强 for：遍历数组/集合 for (int num : new int[]{1, 2, 3}) { System.out.println(num); // 输出 1 2 3 } while / do-while int i = 0; while (i \u0026lt; 3) { // 先判再执行，可能一次都不执行 i++; } do { // 先执行再判，至少执行一次 i++; } while (i \u0026lt; 3); break / continue for (int i = 1; i \u0026lt;= 10; i++) { if (i == 5) break; // 直接退出整个循环 } for (int i = 1; i \u0026lt;= 5; i++) { if (i == 3) continue; // 跳过本次循环，进入下一次 } 6. 数组 一维数组 // 三种创建方式 int[] arr1 = {1, 2, 3, 4, 5}; // 直接初始化 int[] arr2 = new int[]{6, 7, 8}; // new + 初始化 int[] arr3 = new int[4]; // 只指定长度，默认全 0 // 长度 arr1.length // 5 // 访问 arr1[0] = 100; // 改第一个元素 int v = arr1[1]; // 读第二个元素 // arr1[5] // 越界！运行时抛出 ArrayIndexOutOfBoundsException 二维数组 int[][] matrix = { {1, 2, 3}, {4, 5, 6} }; matrix.length // 2（行数） matrix[0].length // 3（第 0 行列数） matrix[0][1] // 2（第 0 行第 1 列） 关键记忆点 文件命名：public class 名必须与文件名一致 入口方法：public static void main(String[] args) 是 JVM 唯一的入口 大小写敏感：Java 严格区分大小写（String ≠ string） 每条语句结束要写分号 ; 数组越界编译不报错、运行时报错 long 字面量加 L，float 字面量加 f == 比较基本类型比值，比较引用类型比地址——String 判断内容用 equals() ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/1.java%E5%9F%BA%E7%A1%80/01-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95/","summary":"\u003ch1 id=\"java-基础语法\"\u003eJava 基础语法\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e参考指导书 2.1 基础语法\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"学习清单\"\u003e学习清单\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e JDK 安装与环境配置\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 第一个 Java 程序：Hello World\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 基本数据类型（int, long, float, double, boolean, char, byte, short）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 变量与常量\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 运算符（算术、关系、逻辑、位、三元）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 流程控制（if-else, switch, for, while, do-while）\u003c/li\u003e\n\u003cli\u003e\u003cinput checked=\"\" disabled=\"\" type=\"checkbox\"\u003e 数组（一维数组、二维数组）\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-jdk-安装与环境配置\"\u003e1. JDK 安装与环境配置\u003c/h2\u003e\n\u003ch3 id=\"jdk-vs-jre\"\u003eJDK vs JRE\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e概念\u003c/th\u003e\n          \u003cth\u003e全称\u003c/th\u003e\n          \u003cth\u003e作用\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eJDK\u003c/td\u003e\n          \u003ctd\u003eJava Development Kit\u003c/td\u003e\n          \u003ctd\u003e开发者工具包，包含 \u003cstrong\u003ejavac（编译器）\u003c/strong\u003e + java（运行器） + JRE\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eJRE\u003c/td\u003e\n          \u003ctd\u003eJava Runtime Environment\u003c/td\u003e\n          \u003ctd\u003e运行 Java 程序的环境，只有 java 没有 javac\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eJVM\u003c/td\u003e\n          \u003ctd\u003eJava Virtual Machine\u003c/td\u003e\n          \u003ctd\u003e执行字节码的虚拟机，JDK 和 JRE 都包含\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"编译与运行流程\"\u003e编译与运行流程\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e.java 源文件  ──javac 编译──▶  .class 字节码  ──java 运行──▶  JVM 执行\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"环境检查\"\u003e环境检查\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejava -version       \u003cspan style=\"color:#75715e\"\u003e# 查看 JDK 版本\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejavac -version      \u003cspan style=\"color:#75715e\"\u003e# 查看编译器版本\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"2-第一个-java-程序hello-world程序基本规则详解\"\u003e2. 第一个 Java 程序：Hello World（程序基本规则详解）\u003c/h2\u003e\n\u003ch3 id=\"最小代码\"\u003e最小代码\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// HelloWorld.java\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eHelloWorld\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e(String\u003cspan style=\"color:#f92672\"\u003e[]\u003c/span\u003e args) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        System.\u003cspan style=\"color:#a6e22e\"\u003eout\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eprintln\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Hello, World!\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"逐行解析\"\u003e逐行解析\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003epublic class HelloWorld\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e","title":"01-Java基础语法"},{"content":"Android 应用层开发学习指导书 从零开始，系统学习 Android 应用层开发，涵盖 Java、Kotlin 及 Android 核心知识。\n目录 学习路线概览 第一阶段：Java 语言基础 第二阶段：Kotlin 语言基础 第三阶段：Android 基础入门 第四阶段：Android 核心组件 第五阶段：Android 进阶 第六阶段：项目实战 学习资源推荐 1. 学习路线概览 Java 基础 → Kotlin 基础 → Android 基础 → Android 核心组件 → Android 进阶 → 项目实战 阶段 内容 建议时长 一 Java 语言基础 2-3 周 二 Kotlin 语言基础 2-3 周 三 Android 基础入门 2 周 四 Android 核心组件 3-4 周 五 Android 进阶 4-6 周 六 项目实战 持续 2. 第一阶段：Java 语言基础 Java 是 Android 开发的基石，理解 Java 有助于深入理解 Android 底层机制。\n2.1 基础语法 JDK 安装与环境配置 第一个 Java 程序：Hello World 基本数据类型（int, long, float, double, boolean, char, byte, short） 变量与常量 运算符（算术、关系、逻辑、位、三元） 流程控制（if-else, switch, for, while, do-while） 数组（一维数组、二维数组） 2.2 面向对象编程（OOP） 类与对象 构造方法 封装（private, default, protected, public） 继承（extends） 多态（方法重载、方法重写） 抽象类（abstract class） 接口（interface） 内部类（成员内部类、静态内部类、匿名内部类、局部内部类） 枚举（enum） 2.3 Java 核心 API String 与 StringBuilder 包装类（Integer, Long, Double 等） Math 类 日期与时间（Date, Calendar, LocalDateTime） 集合框架 List（ArrayList, LinkedList） Set（HashSet, TreeSet） Map（HashMap, TreeMap） 泛型（Generics） 2.4 Java 进阶特性 异常处理（try-catch-finally, throws, 自定义异常） IO 流（File, InputStream, OutputStream, Reader, Writer） 多线程（Thread, Runnable, 线程池 ExecutorService） Lambda 表达式（Java 8+） Stream API（Java 8+） 注解（Annotation） 反射（Reflection） 3. 第二阶段：Kotlin 语言基础 Kotlin 是 Android 官方推荐语言，必须熟练掌握。\n3.1 基础语法 Kotlin 环境搭建（IntelliJ IDEA / Android Studio） 变量声明（val vs var） 基本数据类型 字符串模板 条件表达式（if-else, when） 循环（for, while, do-while） 区间（Range） 3.2 函数 函数声明与调用 默认参数与命名参数 单表达式函数 扩展函数（Extension Function） 高阶函数（Higher-Order Function） Lambda 表达式 内联函数（inline） 3.3 面向对象（Kotlin 风格） 类与构造函数（主构造函数、次构造函数） init 初始化块 属性（getter/setter） 继承与重写（open, override） 抽象类与接口 数据类（data class） 密封类（sealed class） 对象（object）与伴生对象（companion object） 委托（by 关键字） 3.4 空安全（Null Safety） 可空类型（?） 安全调用操作符（?.） Elvis 操作符（?:） 非空断言（!!） let / run / apply / also / with 作用域函数 3.5 集合与序列 List, Set, Map（可变与不可变） 集合操作符（map, filter, reduce, flatMap） 序列（Sequence） 3.6 Kotlin 协程（Coroutines） 协程基础（suspend 函数） 协程构建器（launch, async, runBlocking） 协程上下文与调度器（Dispatchers） 协程作用域（CoroutineScope, viewModelScope, lifecycleScope） 通道（Channel）与流（Flow） 异常处理（try-catch, CoroutineExceptionHandler） 4. 第三阶段：Android 基础入门 4.1 开发环境 Android Studio 安装与配置 Android SDK 管理 AVD（Android Virtual Device）创建 Gradle 构建系统基础 项目目录结构理解 4.2 应用基础 AndroidManifest.xml 清单文件 Activity 与生命周期 Intent（显式 / 隐式） 页面导航与数据传递 资源管理（res/ 目录） 字符串与多语言（strings.xml） 颜色、尺寸、主题（colors.xml, dimens.xml, themes.xml） 4.3 UI 基础（View 体系） 布局管理器 LinearLayout RelativeLayout ConstraintLayout（推荐） FrameLayout TableLayout 常用控件 TextView, EditText Button, ImageButton ImageView CheckBox, RadioButton Switch, ToggleButton ProgressBar, SeekBar RatingBar ScrollView 与 NestedScrollView RecyclerView（列表的核心控件） ViewPager（页面滑动） 4.4 样式与资源 Style 样式定义与继承 Material Design 组件（MaterialButton, Chip, CardView 等） Drawable（shape, layer-list, selector） 屏幕适配基础（dp, sp, px 区别） Vector Drawable 矢量图 5. 第四阶段：Android 核心组件 5.1 Activity 深入 Activity 生命周期完整理解 启动模式（standard, singleTop, singleTask, singleInstance） onSaveInstanceState 状态保存与恢复 Activity 之间数据传递（Intent Bundle） startActivityForResult / ActivityResultContracts 5.2 Fragment Fragment 生命周期 Fragment 创建与使用（FragmentManager） Fragment 与 Activity 通信 Navigation 组件与 Fragment 结合 DialogFragment 5.3 Service 服务 Service 生命周期 启动服务（startService） 绑定服务（bindService） IntentService（已废弃，了解即可） 前台服务（Foreground Service）与通知栏 后台任务限制（Android 8.0+） 5.4 BroadcastReceiver 广播接收器 广播类型（标准广播、有序广播、粘性广播） 静态注册与动态注册 系统广播（网络变化、电量、开机等） 自定义广播 5.5 ContentProvider 内容提供者 ContentProvider 概念与用途 ContentResolver 访问系统数据（通讯录、日历等） 自定义 ContentProvider 5.6 数据存储 SharedPreferences（轻量键值对存储） DataStore（SharedPreferences 替代方案） 文件存储（内部存储 / 外部存储） SQLite 数据库 Room 持久化库（推荐） 6. 第五阶段：Android 进阶 6.1 架构模式 MVC 模式理解 MVP 模式理解 MVVM 模式（Android 推荐） ViewModel 的使用与原理 LiveData / StateFlow 数据观察 DataBinding 数据绑定 ViewBinding 视图绑定 6.2 网络请求 权限声明（INTERNET） 网络请求基本概念（RESTful API, JSON） OkHttp 基础使用 Retrofit 网络框架 Gson / Moshi JSON 解析 网络请求最佳实践（单例、拦截器、缓存） 网络安全配置（Network Security Config） 6.3 图片加载 Glide 图片加载库 Coil（Kotlin 原生，推荐） 图片缓存策略 6.4 异步处理 主线程与子线程 Handler 与 Looper 机制 AsyncTask（已废弃，了解即可） RxJava 基础（可选了解） Kotlin Coroutines 在 Android 中的最佳实践 6.5 依赖注入（DI） 依赖注入概念 Dagger 基础（了解） Hilt（Android 官方推荐 DI 框架） Koin（轻量级替代方案，可选） 6.6 Jetpack 组件 Navigation 导航组件 Room 数据库 ViewModel + LiveData DataStore WorkManager（后台任务调度） Paging 3（分页加载） CameraX（相机） 6.7 Jetpack Compose（现代 UI） Compose 基本概念（声明式 UI） Composable 函数 布局（Column, Row, Box, ConstraintLayout） 状态管理（remember, mutableStateOf, StateFlow 整合） Modifier 修饰符 Compose 中导航 Compose 与 View 体系互操作 6.8 性能优化 布局优化（减少层级、ViewStub, merge） 内存泄漏检测（LeakCanary） 内存优化（避免不必要的对象创建） 启动速度优化（冷启动 / 热启动） APK 体积优化 ANR 问题定位与排查 Profiler 工具使用 6.9 安全基础 数据加密（EncryptedSharedPreferences / Crypto） HTTPS 证书校验 代码混淆（ProGuard / R8） 敏感信息保护（API Key 等） WebView 安全 7. 第六阶段：项目实战 7.1 入门级项目 待办事项应用（Todo List）—— 练习基础 UI 与数据存储 计算器 —— 练习布局与逻辑 记事本 —— 练习 Room 数据库 7.2 中级项目 天气应用 —— 练习网络请求与数据解析 新闻客户端 —— 练习 RecyclerView + Retrofit 音乐播放器 —— 练习 Service + 多媒体 7.3 高级项目 仿微信客户端 —— 练习综合架构设计 电商应用 —— 练习复杂业务逻辑与架构 个人作品集 App —— 展示综合能力 8. 学习资源推荐 官方网站 Android 开发者官网 Kotlin 官方文档 Material Design 推荐书籍 《第一行代码 Android》 —— 郭霖 《Android 编程权威指南》 《Kotlin 实战》 《Effective Java》 《深入理解 Java 虚拟机》（JVM） 在线资源 Google Codelabs Android 开发者培训课程 GitHub 开源项目 附录：每日学习建议 理论学习 + 实践结合：每学一个知识点，立即写代码验证 做好笔记：本目录下按阶段创建对应的 .md 笔记文件 代码托管：使用 Git 管理学习代码 定期复习：每周回顾本周所学内容 坚持输出：用博客或笔记记录学习心得 学习 Android 开发是一个长期过程，保持耐心与好奇心，享受创造的乐趣！\n","permalink":"https://luojmarkdown.pages.dev/posts/%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8/android%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%AF%BC%E4%B9%A6/","summary":"\u003ch1 id=\"android-应用层开发学习指导书\"\u003eAndroid 应用层开发学习指导书\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e从零开始，系统学习 Android 应用层开发，涵盖 Java、Kotlin 及 Android 核心知识。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"目录\"\u003e目录\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"#1-%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E6%A6%82%E8%A7%88\"\u003e学习路线概览\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#2-%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5java-%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80\"\u003e第一阶段：Java 语言基础\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#3-%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5kotlin-%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80\"\u003e第二阶段：Kotlin 语言基础\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#4-%E7%AC%AC%E4%B8%89%E9%98%B6%E6%AE%B5android-%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8\"\u003e第三阶段：Android 基础入门\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#5-%E7%AC%AC%E5%9B%9B%E9%98%B6%E6%AE%B5android-%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6\"\u003e第四阶段：Android 核心组件\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#6-%E7%AC%AC%E4%BA%94%E9%98%B6%E6%AE%B5android-%E8%BF%9B%E9%98%B6\"\u003e第五阶段：Android 进阶\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#7-%E7%AC%AC%E5%85%AD%E9%98%B6%E6%AE%B5%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98\"\u003e第六阶段：项目实战\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#8-%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%BA%90%E6%8E%A8%E8%8D%90\"\u003e学习资源推荐\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-学习路线概览\"\u003e1. 学习路线概览\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eJava 基础 → Kotlin 基础 → Android 基础 → Android 核心组件 → Android 进阶 → 项目实战\n\u003c/code\u003e\u003c/pre\u003e\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e阶段\u003c/th\u003e\n          \u003cth\u003e内容\u003c/th\u003e\n          \u003cth\u003e建议时长\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e一\u003c/td\u003e\n          \u003ctd\u003eJava 语言基础\u003c/td\u003e\n          \u003ctd\u003e2-3 周\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e二\u003c/td\u003e\n          \u003ctd\u003eKotlin 语言基础\u003c/td\u003e\n          \u003ctd\u003e2-3 周\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e三\u003c/td\u003e\n          \u003ctd\u003eAndroid 基础入门\u003c/td\u003e\n          \u003ctd\u003e2 周\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e四\u003c/td\u003e\n          \u003ctd\u003eAndroid 核心组件\u003c/td\u003e\n          \u003ctd\u003e3-4 周\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e五\u003c/td\u003e\n          \u003ctd\u003eAndroid 进阶\u003c/td\u003e\n          \u003ctd\u003e4-6 周\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e六\u003c/td\u003e\n          \u003ctd\u003e项目实战\u003c/td\u003e\n          \u003ctd\u003e持续\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-第一阶段java-语言基础\"\u003e2. 第一阶段：Java 语言基础\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eJava 是 Android 开发的基石，理解 Java 有助于深入理解 Android 底层机制。\u003c/p\u003e","title":"Android 应用层开发学习指导书"},{"content":"Pull 拉下来切换到远程分支\ngit clone xxx git checkout -b localName origin/remoteName 基于远程分支新建开发分支\ngit branch 开发分支名 git branch -d 分支名//删除分支 提交修改 git add git commit/git cz 取消commit 留下commit的修改 git reset --soft \u0026lt;commit_hash\u0026gt;^ //也就是git add 之后，改动留在暂存区 git reset --soft HEAD~ git reset --mixed \u0026lt;commit_hash\u0026gt;^//也就是git add之前，改动留在工作区 git reset --mixed HEAD~ 不保存commit的修改 git reset --hard \u0026lt;commit_hash\u0026gt;^//丢去此次commit的修改 //！！！！注意他会将工作区的内容直接恢复到没有此次commit之前，如果改动还在工作区那么就会被复位 抵消commit git revert//git revert本质上是产生一个新的commit来撤销某个commit的修改，故可以利用git revert来回退版本 push push前务必拉取最新的代码 git pull --no-rebase origin 远程分支名 push前将开发分支的改动cherry-pick过去 //1.首先切换到你想将 commit 迁移到的目标分支 git checkout \u0026lt;目标分支\u0026gt; //2.找到要迁移的 commit git log \u0026lt;开发分支\u0026gt; //3.使用 cherry-pick 迁移 commit git cherry-pick \u0026lt;commit哈希值\u0026gt; //如果要一次性迁移多个 commit，可以使用 .. 范围： git cherry-pick \u0026lt;开始哈希值\u0026gt;^..\u0026lt;结束哈希值\u0026gt; 将改动push到远程 git push origin localName:remoteName git push origin --delete xxx//删除远程分支 rebase与no-rebase remote：A-B-C-D local ：A-B-C-E git pull rebase：//相当于把代码拉下来，重新提交一次E A-B-C-D-‘E’//这个‘E’内容是一样的但是和本地最开始的commit id不同 no rebase：//会进行一次合成，在本地多出一个新的为merge commit M. D ↓ A-B-C-M ↑ E ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%91%BD%E4%BB%A4%E7%9B%B8%E5%85%B3/git/","summary":"\u003ch2 id=\"pull\"\u003ePull\u003c/h2\u003e\n\u003cp\u003e拉下来切换到远程分支\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit clone xxx\ngit checkout -b localName origin/remoteName\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e基于远程分支新建开发分支\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit branch 开发分支名\n\u003c/code\u003e\u003c/pre\u003e\u003cblockquote\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit branch -d 分支名//删除分支\n\u003c/code\u003e\u003c/pre\u003e\u003c/blockquote\u003e\n\u003ch3 id=\"提交修改\"\u003e提交修改\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit add\ngit commit/git cz\n\u003c/code\u003e\u003c/pre\u003e\u003cblockquote\u003e\n\u003ch3 id=\"取消commit\"\u003e取消commit\u003c/h3\u003e\n\u003ch4 id=\"留下commit的修改\"\u003e留下commit的修改\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit reset --soft \u0026lt;commit_hash\u0026gt;^ //也就是git add 之后，改动留在暂存区\ngit reset --soft HEAD~\ngit reset --mixed \u0026lt;commit_hash\u0026gt;^//也就是git add之前，改动留在工作区\ngit reset --mixed HEAD~\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"不保存commit的修改\"\u003e不保存commit的修改\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit reset --hard \u0026lt;commit_hash\u0026gt;^//丢去此次commit的修改\n//！！！！注意他会将工作区的内容直接恢复到没有此次commit之前，如果改动还在工作区那么就会被复位\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"抵消commit\"\u003e抵消commit\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit revert//git revert本质上是产生一个新的commit来撤销某个commit的修改，故可以利用git revert来回退版本\n\u003c/code\u003e\u003c/pre\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"push\"\u003epush\u003c/h2\u003e\n\u003ch4 id=\"push前务必拉取最新的代码\"\u003epush前务必拉取最新的代码\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c++\" data-lang=\"c++\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit pull \u003cspan style=\"color:#f92672\"\u003e--\u003c/span\u003eno\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003erebase origin \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e远程分支名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"push前将开发分支的改动cherry-pick过去\"\u003epush前将开发分支的改动cherry-pick过去\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e//1.首先切换到你想将 commit 迁移到的目标分支\ngit checkout \u0026lt;目标分支\u0026gt;\n//2.找到要迁移的 commit\ngit log \u0026lt;开发分支\u0026gt;\n//3.使用 cherry-pick 迁移 commit\ngit cherry-pick \u0026lt;commit哈希值\u0026gt;\n//如果要一次性迁移多个 commit，可以使用 .. 范围：\ngit cherry-pick \u0026lt;开始哈希值\u0026gt;^..\u0026lt;结束哈希值\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"将改动push到远程\"\u003e将改动push到远程\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c++\" data-lang=\"c++\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit push origin localName:remoteName\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit push origin --delete xxx//删除远程分支\n\u003c/code\u003e\u003c/pre\u003e\u003cblockquote\u003e\n\u003ch3 id=\"rebase与no-rebase\"\u003erebase与no-rebase\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c++\" data-lang=\"c++\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eremote\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e：\u003c/span\u003eA\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eB\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eC\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eD\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elocal \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e：\u003c/span\u003eA\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eB\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eC\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eE\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit pull\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erebase\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e：\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e//相当于把代码拉下来，重新提交一次E\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eA\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eB\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eC\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eD\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e‘\u003c/span\u003eE\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e’\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e//这个‘E’内容是一样的但是和本地最开始的commit id不同 \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eno rebase\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e：\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e//会进行一次合成，在本地多出一个新的为merge commit M.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   D\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e↓\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eA\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eB\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eC\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003eM\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e↑\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   E\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/blockquote\u003e","title":"Git"},{"content":" 关闭自动重启（基于bazeport项目）\nsetprop persist.bazeport.stub.no_reboot 1 唤出设置\nam start com.android.tv.settings/.MainSettings 查看当前的active\ndumpsys window | grep mCurrentFocus 控制台模拟遥控器\ninput keyevent xx input keyevent 19：向上 input keyevent 20：向下 input keyevent 21：向左 input keyevent 22：向右 input keyevent 23：确认（回车） input keyevent 4 ：bakc input keyevent 26 ：power input keyevent KEYCODE_DEL 删除 输入文本\ninput text 文本 ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%91%BD%E4%BB%A4%E7%9B%B8%E5%85%B3/%E5%AE%89%E5%8D%93%E6%9C%BA%E9%A1%B6%E7%9B%92%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/","summary":"\u003ch1\u003e\u003c/h1\u003e\n\u003cp\u003e关闭自动重启（基于bazeport项目）\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c++\" data-lang=\"c++\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esetprop persist.bazeport.stub.no_reboot \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e唤出设置\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eam start com.android.tv.settings/.MainSettings\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看当前的active\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edumpsys window | grep mCurrentFocus\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e控制台模拟遥控器\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003einput keyevent xx\n\ninput keyevent 19：向上\ninput keyevent 20：向下\ninput keyevent 21：向左\ninput keyevent 22：向右\ninput keyevent 23：确认（回车）\ninput keyevent 4 ：bakc\ninput keyevent 26 ：power\ninput keyevent KEYCODE_DEL 删除\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e输入文本\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003einput text 文本\n\u003c/code\u003e\u003c/pre\u003e","title":"安卓机顶盒常用命令"},{"content":"# 设备端启用网络 ADB su setprop service.adb.tcp.port 5555 stop adbd start adbd ip addr show # PC 端连接 adb connect 192.168.141.241:5555 192.168.110.240 192.168.140.29 #root权限连接 adb root # 注意: 连接前确保网络可达 ping \u0026lt;device_ip\u0026gt; ADB 常用命令大全（Markdown 速查） 说明：以下命令默认已将 adb 加入环境变量；如有多台设备，请优先使用 -s \u0026lt;serial\u0026gt; 指定设备（见“多设备/选择目标设备”）。\n0. 基础与排错 adb version # 查看 adb 版本 adb help # 查看帮助 adb start-server # 启动 adb server adb kill-server # 关闭 adb server adb nodaemon server # 前台启动 server（便于看日志） adb wait-for-device # 阻塞直到设备连接可用 adb reconnect # 重新连接设备（USB/TCP） adb reconnect device # 重新连接设备端 adb reconnect offline # 尝试重连离线设备 常见排错：\nadb devices -l # 查看设备列表（带更多信息） adb usb # 切回 USB 模式 adb tcpip 5555 # 切到 TCP 模式（需 USB 已连接一次） adb connect \u0026lt;ip\u0026gt;:5555 # 通过网络连接 adb disconnect \u0026lt;ip\u0026gt;:5555 # 断开指定网络设备 adb disconnect # 断开全部网络设备 1. 多设备/选择目标设备 adb devices # 列出已连接设备 adb devices -l # 更详细信息 adb -s \u0026lt;serial\u0026gt; shell # 对指定设备执行命令 adb -d shell # 仅对 USB 设备（单一）执行 adb -e shell # 仅对模拟器执行 adb -t \u0026lt;transport_id\u0026gt; shell # Android 11+ 可用：按 transport_id 选择 获取序列号：\nadb get-serialno adb -s \u0026lt;serial\u0026gt; get-serialno 2. 设备信息 adb shell getprop # 查看全部系统属性 adb shell getprop ro.product.model # 机型 adb shell getprop ro.build.version.release # Android 版本 adb shell getprop ro.build.version.sdk # SDK 版本 adb shell settings get system screen_brightness # 读取系统设置（示例） adb shell wm size # 屏幕分辨率 adb shell wm density # 屏幕 DPI adb shell df -h # 存储情况 adb shell uptime # 运行时长 3. 安装/卸载/应用管理（pm） 安装：\nadb install app.apk # 安装 adb install -r app.apk # 覆盖安装（保留数据） adb install -d app.apk # 允许版本降级 adb install -g app.apk # 安装时授予所有运行时权限 adb install-multiple base.apk split_*.apk # 安装 split APK adb install --user 0 app.apk # 为 user 0 安装（部分 ROM/场景） 卸载：\nadb uninstall \u0026lt;package\u0026gt; # 卸载（删除数据） adb uninstall -k \u0026lt;package\u0026gt; # 卸载但保留数据与缓存（若支持） 列出/查询：\nadb shell pm list packages # 列出所有包名 adb shell pm list packages -3 # 仅第三方应用 adb shell pm list packages -s # 仅系统应用 adb shell pm list packages \u0026lt;keyword\u0026gt; # 按关键字过滤 adb shell pm path \u0026lt;package\u0026gt; # 查看 APK 路径 adb shell pm dump \u0026lt;package\u0026gt; # 输出包信息（较多） adb shell cmd package resolve-activity --brief \u0026lt;intent\u0026gt; # 解析 Activity（新命令体系） 清理数据/授权：\nadb shell pm clear \u0026lt;package\u0026gt; # 清除应用数据 adb shell pm grant \u0026lt;package\u0026gt; \u0026lt;permission\u0026gt; # 授予权限 adb shell pm revoke \u0026lt;package\u0026gt; \u0026lt;permission\u0026gt; # 撤销权限 启用/停用：\nadb shell pm disable-user --user 0 \u0026lt;package\u0026gt; # 对 user 0 停用 adb shell pm enable \u0026lt;package\u0026gt; # 启用 adb shell pm uninstall --user 0 \u0026lt;package\u0026gt; # 仅对 user 0 卸载（系统应用常用） 4. 启动/停止应用与 Activity（am） 启动应用主界面：\nadb shell monkey -p \u0026lt;package\u0026gt; -c android.intent.category.LAUNCHER 1 启动 Activity：\nadb shell am start -n \u0026lt;package\u0026gt;/\u0026lt;activity\u0026gt; # 指定组件启动 adb shell am start -a android.intent.action.VIEW -d \u0026#34;https://example.com\u0026#34; 启动 Service / 发送广播：\nadb shell am startservice -n \u0026lt;package\u0026gt;/\u0026lt;service\u0026gt; adb shell am broadcast -a \u0026lt;action\u0026gt; --es key value 强制停止/查看当前前台：\nadb shell am force-stop \u0026lt;package\u0026gt; # 强制停止 adb shell dumpsys activity activities | head # 查看 Activity 栈（输出很多） adb shell dumpsys window | grep mCurrentFocus # 当前焦点窗口（部分系统可用） 5. 进入 Shell 与常用 Linux/Android 命令 adb shell # 进入交互式 shell adb shell \u0026lt;cmd\u0026gt; # 直接执行单条命令 adb shell \u0026#34;cd /sdcard \u0026amp;\u0026amp; ls\u0026#34; # 执行复合命令 常见：\nadb shell ls /sdcard adb shell pwd adb shell cat /proc/cpuinfo adb shell ps -A # 进程列表（不同版本参数略有差异） adb shell top -n 1 adb shell id # 当前用户信息 adb shell su # root（仅已 root/工程机） 6. 文件传输（push/pull）与目录管理 adb push \u0026lt;local\u0026gt; \u0026lt;remote\u0026gt; # 推送到设备 adb pull \u0026lt;remote\u0026gt; \u0026lt;local\u0026gt; # 从设备拉取 adb pull \u0026lt;remote\u0026gt; # 拉取到当前目录 adb shell mkdir -p /sdcard/test adb shell rm -rf /sdcard/test 提示：Android 10+ 分区存储影响 /sdcard/Android/data 等目录访问，可能需要 run-as、root 或通过应用自身导出。\n7. 截图/录屏 截图：\nadb shell screencap -p /sdcard/screen.png adb pull /sdcard/screen.png . adb shell rm /sdcard/screen.png 直接输出到本机文件（避免占用手机存储）：\nadb exec-out screencap -p \u0026gt; screen.png //这样输出的文件似乎会损坏 录屏：\nadb shell screenrecord /sdcard/demo.mp4 adb pull /sdcard/demo.mp4 . adb shell rm /sdcard/demo.mp4 常用参数：\nadb shell screenrecord --time-limit 30 /sdcard/demo.mp4 adb shell screenrecord --size 1280x720 --bit-rate 6000000 /sdcard/demo.mp4 8. 日志（logcat）与抓取问题现场 logcat：\nadb logcat # 实时日志 adb logcat -c # 清空缓冲区 adb logcat -d \u0026gt; logcat.txt # 导出当前日志并退出 adb logcat -v time # 带时间格式 adb logcat *:W # 仅 Warning 及以上 adb logcat \u0026lt;TAG\u0026gt;:V *:S # 仅某 TAG adb logcat --pid=$(adb shell pidof \u0026lt;package\u0026gt;) # 仅某进程（新版本支持） logcat -s Activity 抓取更完整的诊断：\nadb bugreport \u0026gt; bugreport.zip # Android 7+ 通常输出 zip adb shell dumpsys # 输出大量系统服务状态 adb shell dumpsys meminfo \u0026lt;package\u0026gt; # 内存信息 adb shell dumpsys cpuinfo # CPU 信息（老设备常用） 9. 模拟输入（input） 点击/滑动/文本：\nadb shell input tap 500 1200 adb shell input swipe 200 1200 200 300 # 向上滑动 adb shell input swipe 200 300 200 1200 500 # 带时长（ms） adb shell input text \u0026#34;hello_world\u0026#34; # 空格需转义或用 %s adb shell input keyevent 3 # HOME adb shell input keyevent 4 # BACK adb shell input keyevent 26 # POWER adb shell input keyevent 24 # VOLUME_UP adb shell input keyevent 25 # VOLUME_DOWN 常用 KeyEvent（部分）：\n3 HOME, 4 BACK, 19/20/21/22 方向键, 23 DPAD_CENTER/ENTER, 66 ENTER, 67 DEL, 82 MENU, 187 APP_SWITCH 10. 端口转发/反向转发（调试必备） adb forward tcp:6100 tcp:7100 # 本机 6100 -\u0026gt; 设备 7100 adb reverse tcp:8081 tcp:8081 # 设备 8081 -\u0026gt; 本机 8081（React Native 常用） adb forward --list # 查看转发列表 adb forward --remove tcp:6100 adb forward --remove-all adb reverse --list adb reverse --remove tcp:8081 adb reverse --remove-all 11. 重启与模式切换 adb reboot # 重启系统 adb reboot recovery # 重启到 recovery adb reboot bootloader # 重启到 bootloader/fastboot adb reboot sideload # 重启到 sideload（部分机型/Recovery 支持） adb reboot-bootloader # 等价写法（部分环境） 12. Root / 分区挂载（需要设备支持） 仅工程机/userdebug/已 root 设备可用，商用机通常不可用。\nadb root # 以 root 重启 adbd adb unroot # 取消 root adb remount # 重新挂载系统分区为可写（需要 root） adb disable-verity # 禁用 verity（危险，谨慎） adb enable-verity # 启用 verity 13. run-as（调试应用私有目录） 仅对 debuggable 应用有效（通常 debug 包可用）。\nadb shell run-as \u0026lt;package\u0026gt; ls adb shell run-as \u0026lt;package\u0026gt; cat files/config.json adb shell run-as \u0026lt;package\u0026gt; cp files/a.db /sdcard/a.db adb pull /sdcard/a.db . 14. AppOps / 权限与设置（进阶） adb shell appops get \u0026lt;package\u0026gt; adb shell appops set \u0026lt;package\u0026gt; \u0026lt;OP\u0026gt; allow|deny|ignore adb shell settings list system adb shell settings list secure adb shell settings list global adb shell settings get secure android_id adb shell settings put global animator_duration_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global window_animation_scale 0 15. 复制/粘贴与剪贴板（部分版本/ROM 支持） adb shell cmd clipboard get # Android 10+ 常见 adb shell cmd clipboard set \u0026#34;hello\u0026#34; # 可能因 ROM/权限限制不可用 16. 常见组合用法示例 查看并过滤应用日志：\nadb shell pidof \u0026lt;package\u0026gt; adb logcat --pid=\u0026lt;pid\u0026gt; -v time 从 URL 拉起浏览器：\nadb shell am start -a android.intent.action.VIEW -d \u0026#34;https://www.android.com\u0026#34; 获取当前前台包名（不同版本可能不一致）：\nadb shell dumpsys window | grep -E \u0026#34;mCurrentFocus|mFocusedApp\u0026#34; 17. 备忘：常用参数与注意点 -s \u0026lt;serial\u0026gt;：多设备时指定目标设备（强烈建议养成习惯） adb exec-out ...：直接把设备输出流重定向到本机文件（截图/录屏/导出更方便） Android 10+：分区存储导致部分目录不可读；优先用应用内导出、run-as 或调试接口 某些命令在不同 Android 版本/ROM 上存在差异（如 ps 参数、剪贴板命令等） ","permalink":"https://luojmarkdown.pages.dev/posts/%E5%91%BD%E4%BB%A4%E7%9B%B8%E5%85%B3/adb%E7%9B%B8%E5%85%B3/","summary":"\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# 设备端启用网络 ADB\nsu\nsetprop service.adb.tcp.port 5555\nstop adbd\nstart adbd\n\nip addr show\n\n# PC 端连接\nadb connect 192.168.141.241:5555\n    192.168.110.240\n    192.168.140.29\n\n#root权限连接\nadb root\n\n# 注意: 连接前确保网络可达\nping \u0026lt;device_ip\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"adb-常用命令大全markdown-速查\"\u003eADB 常用命令大全（Markdown 速查）\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e说明：以下命令默认已将 \u003ccode\u003eadb\u003c/code\u003e 加入环境变量；如有多台设备，请优先使用 \u003ccode\u003e-s \u0026lt;serial\u0026gt;\u003c/code\u003e 指定设备（见“多设备/选择目标设备”）。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"0-基础与排错\"\u003e0. 基础与排错\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb version                  \u003cspan style=\"color:#75715e\"\u003e# 查看 adb 版本\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb help                     \u003cspan style=\"color:#75715e\"\u003e# 查看帮助\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb start-server             \u003cspan style=\"color:#75715e\"\u003e# 启动 adb server\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb kill-server              \u003cspan style=\"color:#75715e\"\u003e# 关闭 adb server\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb nodaemon server          \u003cspan style=\"color:#75715e\"\u003e# 前台启动 server（便于看日志）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb wait-for-device          \u003cspan style=\"color:#75715e\"\u003e# 阻塞直到设备连接可用\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb reconnect                \u003cspan style=\"color:#75715e\"\u003e# 重新连接设备（USB/TCP）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb reconnect device         \u003cspan style=\"color:#75715e\"\u003e# 重新连接设备端\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eadb reconnect offline        \u003cspan style=\"color:#75715e\"\u003e# 尝试重连离线设备\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e常见排错：\u003c/p\u003e","title":"ADB相关"}]