[linux 驱动]input输入子系统详解与实战

news2024/11/6 7:21:10

目录

1 描述

2 结构体

2.1 input_class

2.2 input_dev

2.4 input_event

2.4 input_dev_type

3 input接口

3.1 input_allocate_device

3.2 input_free_device

3.3 input_register_device

3.4 input_unregister_device

3.5 input_event

3.6 input_sync

3.7 input_set_capability

4 input.c分析

4.1 注册字符设备

4.2 函数分析

4.2.1 class_register

4.2.2 dev_name

4.2.3 register_chrdev_region

4.2.3 MKDEV

5 示例

5.1 示例 1

5.1.1 代码

5.1.2 操作

5.2 mcu_cec驱动分析

5.2.1 注册输入事件

5.2.2 函数分析

5.2.2.1 devm_input_allocate_device

5.2.3 上报事件


1 描述

        input 子系统分为 input 驱动层、input 核心层、input 事件处理层,最终给用户空间提供可访问的设备节点,input 子系统框架如图

驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。

核心层:承上启下,为驱动层提供输入设备 注册和操作接口。通知事件层对输入事件进行 处理。

事件层:主要和用户空间进行交互。

        input 核心层会向 Linux 内核注册一个字符设备,在 drivers/input/input.c文件中描述

        如何找到input 设备节点文件对应的具体设备?

        方法(1):使用sudo cat 命令打开/dev/input/下的设备节点文件,然后分别操作各个输入设备,如果有输出有乱码,说明此时打开的设备文件就对应于当前的输入设备。

        方法(2):使用sudo hexdump 命令打开/dev/input/下设备文件,然后分别操作各个输入设备,如果有输出16进制数,说明此时打开的设备文件就对应于当前的输入设备。

        方法(3):查看/proc/bus/input/devices文件,里面记载了各个已经安装输入设备的信息。通过文件内容name和handler来查看设备文件与设备的对应关系。

127|rk3399_Android11:/ # cat /proc/bus/input/devices
I: Bus=0019 Vendor=524b Product=0006 Version=0100
N: Name="ff420030.pwm"
P: Phys=gpio-keys/remotectl
S: Sysfs=/devices/platform/ff420030.pwm/input/input0
U: Uniq=
H: Handlers=event0 cpufreq dmcfreq
B: PROP=0
B: EV=3
B: KEY=70010 20000000000000 0 100010002000000 78000004000a800 1e16c000000000 10004ffc

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="adc_keys"
P: Phys=adc-keys/input0
S: Sysfs=/devices/platform/adc_keys/input/input1
U: Uniq=
H: Handlers=event1 cpufreq dmcfreq
B: PROP=0
B: EV=3
B: KEY=c000000000000 0

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="gpio-keys"
P: Phys=gpio-keys/input0
S: Sysfs=/devices/platform/gpio-keys/input/input2
U: Uniq=
H: Handlers=event2 cpufreq dmcfreq
B: PROP=0
B: EV=100003
B: KEY=10000000000000 0

I: Bus=0000 Vendor=0001 Product=0001 Version=0100
N: Name="rk-headset"
P: Phys=
S: Sysfs=/devices/platform/rk-headset/input/input3
U: Uniq=
H: Handlers=event3
B: PROP=0
B: EV=3
B: KEY=400000000 0 0 0

rk3399_Android11:/ #

2 结构体

2.1 input_class

1782 struct class input_class = {
1783         .name           = "input",
1784         .devnode        = input_devnode,
1785 };

2.2 input_dev

        input_dev 结构体是用于描述和管理输入设备的复杂数据结构,涵盖了设备的基本信息、支持的事件、状态管理、操作函数等多个方面,是 Linux 输入子系统的重要组成部分。

130 struct input_dev {
131         const char *name;
132         const char *phys;
133         const char *uniq;
134         struct input_id id;
135 
136         unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
137 
138         unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
139         unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
140         unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
141         unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
142         unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
143         unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
144         unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
145         unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
146         unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
147 
148         unsigned int hint_events_per_packet;
149 
150         unsigned int keycodemax;
151         unsigned int keycodesize;
152         void *keycode;
153 
154         int (*setkeycode)(struct input_dev *dev,
155                           const struct input_keymap_entry *ke,
156                           unsigned int *old_keycode);
157         int (*getkeycode)(struct input_dev *dev,
158                           struct input_keymap_entry *ke);
159 
160         struct ff_device *ff;
161 
162         unsigned int repeat_key;
163         struct timer_list timer;
164 
165         int rep[REP_CNT];
166 
167         struct input_mt *mt;
168 
169         struct input_absinfo *absinfo;
170 
171         unsigned long key[BITS_TO_LONGS(KEY_CNT)];
172         unsigned long led[BITS_TO_LONGS(LED_CNT)];
173         unsigned long snd[BITS_TO_LONGS(SND_CNT)];
174         unsigned long sw[BITS_TO_LONGS(SW_CNT)];
175 
176         int (*open)(struct input_dev *dev);
177         void (*close)(struct input_dev *dev);
178         int (*flush)(struct input_dev *dev, struct file *file);
179         int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
180 
181         struct input_handle __rcu *grab;
182 
183         spinlock_t event_lock;
184         struct mutex mutex;
185 
186         unsigned int users;
187         bool going_away;
188 
189         struct device dev;
190 
191         struct list_head        h_list;
192         struct list_head        node;
193 
194         unsigned int num_vals;
195         unsigned int max_vals;
196         struct input_value *vals;
197 
198         bool devres_managed;
199 
200         ktime_t timestamp[INPUT_CLK_MAX];
201 };

基本信息

const char *name;

const char *phys;

const char *uniq;

name: 输入设备的名称。

phys: 物理路径,通常用于描述设备的实际连接地址。

uniq: 设备的唯一标识符,用于区分不同设备。

设备 ID

struct input_id id;

id: 包含设备类型、制造商和产品 ID 的结构体,用于唯一标识输入设备。

属性位

unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

这些位字段用于表示设备的特性(如支持哪些输入属性)。

evbit: 表示设备支持的事件类型(如按键、鼠标移动等)。

keybit: 表示支持的按键。

relbit: 表示支持的相对坐标变化事件(如鼠标移动)。

absbit: 表示支持的绝对坐标事件。

mscbit: 表示支持的特殊事件类型。

ledbit: 表示支持的 LED 控制。

sndbit: 表示支持的声音控制。

ffbit: 表示支持的力反馈事件。

swbit: 表示支持的开关状态。

键盘设置

unsigned int keycodemax;

unsigned int keycodesize;

void *keycode;

keycodemax: 最大键码数量。

keycodesize: 每个键码的大小。

keycode: 指向键码数组的指针。

键码处理函数

int (*setkeycode)(struct input_dev *dev, const struct input_keymap_entry *ke, unsigned int *old_keycode);

int (*getkeycode)(struct input_dev *dev, struct input_keymap_entry *ke);

setkeycode: 设置键码的回调函数。

getkeycode: 获取键码的回调函数。

其他功能

unsigned int repeat_key;

struct timer_list timer;

repeat_key: 用于设置键重复的时间间隔。

timer: 定时器,用于处理键重复事件。

多点触控和绝对信息

struct input_mt *mt;

struct input_absinfo *absinfo;

mt: 多点触控相关信息。

absinfo: 绝对坐标信息。

状态管理

unsigned long key[BITS_TO_LONGS(KEY_CNT)];

unsigned long led[BITS_TO_LONGS(LED_CNT)];

unsigned long snd[BITS_TO_LONGS(SND_CNT)];

unsigned long sw[BITS_TO_LONGS(SW_CNT)];

用于存储设备当前状态的信息,包括按键状态、LED 状态、声音状态和开关状态。

设备操作函数

int (*open)(struct input_dev *dev);

void (*close)(struct input_dev *dev);

int (*flush)(struct input_dev *dev, struct file *file);

int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);

open: 打开设备的回调函数。

close: 关闭设备的回调函数。

flush: 刷新设备状态的回调函数。

event: 处理输入事件的回调函数。

锁和同步

spinlock_t event_lock;

struct mutex mutex;

event_lock: 自旋锁,用于保护事件队列。

mutex: 互斥锁,用于设备操作的同步。

引用计数和状态

unsigned int users;

bool going_away;

users: 当前打开设备的用户数量。

going_away: 表示设备是否正在被移除。

设备结构

struct device dev;

dev: 表示内核设备结构,包含设备的基本信息。

列表和管理

struct list_head h_list;

struct list_head node;

h_list: 用于哈希表中的链表节点。

node: 用于设备列表中的链表节点。

值管理

unsigned int num_vals;

unsigned int max_vals;

struct input_value *vals;

num_vals: 当前值的数量。

max_vals: 最大值数量。

vals: 指向输入值数组的指针。

设备资源管理

bool devres_managed;

devres_managed: 表示设备资源是否由设备管理系统管理。

时间戳

ktime_t timestamp[INPUT_CLK_MAX];

存储输入事件的时间戳。

2.4 input_event

        Linux 内核使用 input_event 这个结构体来表示所有的输入事件

28 struct input_event {
 29 #if (__BITS_PER_LONG != 32 || !defined(__USE_TIME_BITS64)) && !defined(__KERNEL__)
 30         struct timeval time;
 31 #define input_event_sec time.tv_sec
 32 #define input_event_usec time.tv_usec
 33 #else    
 34         __kernel_ulong_t __sec;
 35 #if defined(__sparc__) && defined(__arch64__)
 36         unsigned int __usec;
 37         unsigned int __pad;
 38 #else    
 39         __kernel_ulong_t __usec;
 40 #endif
 41 #define input_event_sec  __sec
 42 #define input_event_usec __usec
 43 #endif
 44         __u16 type;
 45         __u16 code;
 46         __s32 value;
 47 };

type事件类型,比如 EV_KEY,表示此次事件为按键事件,此成员变量为 16 位。

code:事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如:KEY_0、KEY_1等等这些按键。此成员变量为 16 位。

value:值,比如 EV_KEY 事件中 value 就是按键值,表示按键有没有被按下,如果为 1 的话说明按键按下,如果为 0 的话说明按键没有被按下或者按键松开了。

2.4 input_dev_type

1768 static const struct device_type input_dev_type = {
1769         .groups         = input_dev_attr_groups,
1770         .release        = input_dev_release,
1771         .uevent         = input_dev_uevent,
1772 #ifdef CONFIG_PM_SLEEP
1773         .pm             = &input_dev_pm_ops,
1774 #endif
1775 };
1519 static const struct attribute_group *input_dev_attr_groups[] = {
1520         &input_dev_attr_group,
1521         &input_dev_id_attr_group,
1522         &input_dev_caps_attr_group,
1523         NULL
1524 };
1526 static void input_dev_release(struct device *device)
1527 {
1528         struct input_dev *dev = to_input_dev(device);
1529 
1530         input_ff_destroy(dev);
1531         input_mt_destroy_slots(dev);
1532         kfree(dev->absinfo);
1533         kfree(dev->vals);
1534         kfree(dev);
1535 
1536         module_put(THIS_MODULE);
1537 }
1759 static const struct dev_pm_ops input_dev_pm_ops = {
1760         .suspend        = input_dev_suspend,
1761         .resume         = input_dev_resume,
1762         .freeze         = input_dev_freeze,
1763         .poweroff       = input_dev_poweroff,
1764         .restore        = input_dev_resume,
1765 };

3 input接口

3.1 input_allocate_device

函数原型

struct input_dev *input_allocate_device(void)

返回值

struct input_dev *

成功:input_dev结构体指针 失败:NULL

功能

申请一个 input_dev

1797 struct input_dev *input_allocate_device(void)
1798 {  
1799         static atomic_t input_no = ATOMIC_INIT(-1);
1800         struct input_dev *dev;
1801    
1802         dev = kzalloc(sizeof(*dev), GFP_KERNEL);
1803         if (dev) {
1804                 dev->dev.type = &input_dev_type;
1805                 dev->dev.class = &input_class;
1806                 device_initialize(&dev->dev);
1807                 mutex_init(&dev->mutex);
1808                 spin_lock_init(&dev->event_lock);
1809                 timer_setup(&dev->timer, NULL, 0);
1810                 INIT_LIST_HEAD(&dev->h_list);
1811                 INIT_LIST_HEAD(&dev->node);
1812    
1813                 dev_set_name(&dev->dev, "input%lu",
1814                              (unsigned long)atomic_inc_return(&input_no));
1815    
1816                 __module_get(THIS_MODULE);
1817         }
1818    
1819         return dev;
1820 }  
1768 static const struct device_type input_dev_type = {
1769         .groups         = input_dev_attr_groups,
1770         .release        = input_dev_release,
1771         .uevent         = input_dev_uevent,
1772 #ifdef CONFIG_PM_SLEEP
1773         .pm             = &input_dev_pm_ops,
1774 #endif
1775 };

3.2 input_free_device

函数原型

void input_free_device(struct input_dev *dev)

参数

struct input_dev *

input_dev结构体指针

功能

释放一个 input_dev

1902 void input_free_device(struct input_dev *dev)
1903 {
1904         if (dev) {
1905                 if (dev->devres_managed)
1906                         WARN_ON(devres_destroy(dev->dev.parent,
1907                                                 devm_input_device_release,
1908                                                 devm_input_device_match,
1909                                                 dev));
1910                 input_put_device(dev);
1911         }
1912 }

3.3 input_register_device

函数原型

int input_register_device(struct input_dev *dev)

参数

struct input_dev *dev

input_dev结构体指针

返回值

int

成功:0 失败:错误码

功能

向 Linux 内核注册 input_dev

2140 int input_register_device(struct input_dev *dev)
2141 {
2142         struct input_devres *devres = NULL;
2143         struct input_handler *handler;
2144         unsigned int packet_size;
2145         const char *path;
2146         int error;
2147 
2148         if (test_bit(EV_ABS, dev->evbit) && !dev->absinfo) {
2149                 dev_err(&dev->dev,
2150                         "Absolute device without dev->absinfo, refusing to register\n");
2151                 return -EINVAL;
2152         }
2153 
2154         if (dev->devres_managed) {
2155                 devres = devres_alloc(devm_input_device_unregister,
2156                                       sizeof(*devres), GFP_KERNEL);
2157                 if (!devres)
2158                         return -ENOMEM;
2159 
2160                 devres->input = dev;
2161         }
2162 
2163         /* Every input device generates EV_SYN/SYN_REPORT events. */
2164         __set_bit(EV_SYN, dev->evbit);
2165 
2166         /* KEY_RESERVED is not supposed to be transmitted to userspace. */
2167         __clear_bit(KEY_RESERVED, dev->keybit);
2168 
2169         /* Make sure that bitmasks not mentioned in dev->evbit are clean. */
2170         input_cleanse_bitmasks(dev);
2171 
2172         packet_size = input_estimate_events_per_packet(dev);
2173         if (dev->hint_events_per_packet < packet_size)
2174                 dev->hint_events_per_packet = packet_size;
2175 
2176         dev->max_vals = dev->hint_events_per_packet + 2;
2177         dev->vals = kcalloc(dev->max_vals, sizeof(*dev->vals), GFP_KERNEL);
2178         if (!dev->vals) {
2179                 error = -ENOMEM;
2180                 goto err_devres_free;
2181         }
2182 
2183         /*
2184          * If delay and period are pre-set by the driver, then autorepeating
2185          * is handled by the driver itself and we don't do it in input.c.
2186          */
2187         if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD])
2188                 input_enable_softrepeat(dev, 250, 33);
2189 
2190         if (!dev->getkeycode)
2191                 dev->getkeycode = input_default_getkeycode;
2192 
2193         if (!dev->setkeycode)
2194                 dev->setkeycode = input_default_setkeycode;
2195 
2196         error = device_add(&dev->dev);
2197         if (error)
2198                 goto err_free_vals;
2199 
2200         path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL);
2201         pr_info("%s as %s\n",
2202                 dev->name ? dev->name : "Unspecified device",
2203                 path ? path : "N/A");
2204         kfree(path);
2205 
2206         error = mutex_lock_interruptible(&input_mutex);
2207         if (error)
2208                 goto err_device_del;
2209 
2210         list_add_tail(&dev->node, &input_dev_list);
2211 
2212         list_for_each_entry(handler, &input_handler_list, node)
2213                 input_attach_handler(dev, handler);
2214 
2215         input_wakeup_procfs_readers();
2216 
2217         mutex_unlock(&input_mutex);
2218 
2219         if (dev->devres_managed) {
2220                 dev_dbg(dev->dev.parent, "%s: registering %s with devres.\n",
2221                         __func__, dev_name(&dev->dev));
2222                 devres_add(dev->dev.parent, devres);
2223         }
2224         return 0;
2225 
2226 err_device_del:
2227         device_del(&dev->dev);
2228 err_free_vals:
2229         kfree(dev->vals);
2230         dev->vals = NULL;
2231 err_devres_free:
2232         devres_free(devres);
2233         return error;
2234 }

3.4 input_unregister_device

函数原型

void input_unregister_device(struct input_dev *dev)

参数

struct input_dev *dev

input_dev结构体指针

返回值

功能

向 Linux 内核注销input_dev

2244 void input_unregister_device(struct input_dev *dev)
2245 {
2246         if (dev->devres_managed) {
2247                 WARN_ON(devres_destroy(dev->dev.parent,
2248                                         devm_input_device_unregister,
2249                                         devm_input_device_match,
2250                                         dev));
2251                 __input_unregister_device(dev);
2252                 /*
2253                  * We do not do input_put_device() here because it will be done
2254                  * when 2nd devres fires up.
2255                  */
2256         } else {
2257                 __input_unregister_device(dev);
2258                 input_put_device(dev);
2259         }
2260 }

3.5 input_event

函数原型

void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)

参数

struct input_dev *dev

需要上报的 input_dev

unsigned int type

上报的事件类型,比如 EV_KEY

unsigned int code

事件码,也就是我们注册的按键值,比如 KEY_0、KEY_1

int value

事件值,比如 1 表示按键按下,0 表示按键松开

返回值

功能

上报指定的事件以及对应的值

436 void input_event(struct input_dev *dev,
 437                  unsigned int type, unsigned int code, int value)
 438 {
 439         unsigned long flags;
 440 
 441         if (is_event_supported(type, dev->evbit, EV_MAX)) {
 442 
 443                 spin_lock_irqsave(&dev->event_lock, flags);
 444                 input_handle_event(dev, type, code, value);
 445                 spin_unlock_irqrestore(&dev->event_lock, flags);
 446         }
 447 }

3.6 input_sync

函数原型

static inline void input_sync(struct input_dev *dev)

参数

struct input_dev *dev

需要上报的 input_dev

返回值

功能

告诉 Linux 内核 input 子系统上报结束,input_sync 函数本质是上报一个同步事件

430 static inline void input_sync(struct input_dev *dev)
431 {        
432         input_event(dev, EV_SYN, SYN_REPORT, 0);
433 }    

3.7 input_set_capability

函数原型

void input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code)

参数

struct input_dev *dev

输入设备结构体指针

unsigned int type

输入事件的类型

unsigned int code

事件的特定代码

返回值

功能

向一个已注册的输入设备添加特定的输入能力

1964 void input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code)
1965 {
1966         switch (type) {
1967         case EV_KEY:
1968                 __set_bit(code, dev->keybit);
1969                 break;
1970 
1971         case EV_REL:
1972                 __set_bit(code, dev->relbit);
1973                 break;
1974 
1975         case EV_ABS:
1976                 input_alloc_absinfo(dev);
1977                 if (!dev->absinfo)
1978                         return;
1979 
1980                 __set_bit(code, dev->absbit);
1981                 break;
1982 
1983         case EV_MSC:
1984                 __set_bit(code, dev->mscbit);
1985                 break;
1986 
1987         case EV_SW:
1988                 __set_bit(code, dev->swbit);
1989                 break;
1990 
1991         case EV_LED:
1992                 __set_bit(code, dev->ledbit);
1993                 break;
1994 
1995         case EV_SND:
1996                 __set_bit(code, dev->sndbit);
1997                 break;
1998 
1999         case EV_FF:
2000                 __set_bit(code, dev->ffbit);
2001                 break;
2002 
2003         case EV_PWR:
2004                 /* do nothing */
2005                 break;
2006 
2007         default:
2008                 pr_err("%s: unknown type %u (code %u)\n", __func__, type, code);
2009                 dump_stack();
2010                 return;
2011         }
2012 
2013         __set_bit(type, dev->evbit);
2014 }

4 input.c分析

4.1 注册字符设备

        input 核心层会向 Linux 内核注册一个字符设备,class_register函数注册一个 input 类,这样系统启动以后就会在/sys/class 目录下有一个 input 子目录

zwzn2064@zwzn2064-CVN-Z690D5-GAMING-PRO:~$ ls /sys/class
ata_device   devfreq-event  i2c-dev   nvme            printer       scsi_generic  vc
ata_link     dma            input     nvme-subsystem  ptp           scsi_host     vfio
ata_port     dmi            iommu     pci_bus         pwm           sound         virtio-ports
backlight    extcon         leds      pci_epc         rapidio_port  spi_master    vtconsole
bdi          firmware       mdio_bus  phy             regulator     spi_slave     wakeup
block        gpio           mem       powercap        remoteproc    thermal       watchdog
bsg          graphics       misc      power_supply    rfkill        tpm           wmi_bus
dax          hidraw         mmc_host  ppdev           rtc           tpmrm
devcoredump  hwmon          nd        ppp             scsi_device   tty
devfreq      i2c-adapter    net       pps             scsi_disk     usbmisc
zwzn2064@zwzn2064-CVN-Z690D5-GAMING-PRO:~$ ls /sys/class/input/
event0   event11  event3  event6  event9  input10  input2  input5  input8
event1   event12  event4  event7  input0  input11  input3  input6  input9
event10  event2   event5  event8  input1  input12  input4  input7  mice
zwzn2064@zwzn2064-CVN-Z690D5-GAMING-PRO:~$ 

代码如下所示

#define INPUT_MAJOR             13
36 #define INPUT_MAX_CHAR_DEVICES          1024

1777 static char *input_devnode(struct device *dev, umode_t *mode)
1778 {
1779         return kasprintf(GFP_KERNEL, "input/%s", dev_name(dev));
1780 }

1782 struct class input_class = {
1783         .name           = "input",
1784         .devnode        = input_devnode,
1785 };

2476 static int __init input_init(void)
2477 {
2478         int err;
2479 
2480         err = class_register(&input_class);
2481         if (err) {
2482                 pr_err("unable to register input_dev class\n");
2483                 return err;
2484         }
2485 
2486         err = input_proc_init();
2487         if (err)
2488                 goto fail1;
2489 
2490         err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
2491                                      INPUT_MAX_CHAR_DEVICES, "input");
2492         if (err) {
2493                 pr_err("unable to register char major %d", INPUT_MAJOR);
2494                 goto fail2;
2495         }
2496 
2497         return 0;
2498 
2499  fail2: input_proc_exit();
2500  fail1: class_unregister(&input_class);
2501         return err;
2502 }
2503 
2504 static void __exit input_exit(void)
2505 {
2506         input_proc_exit();
2507         unregister_chrdev_region(MKDEV(INPUT_MAJOR, 0),
2508                                  INPUT_MAX_CHAR_DEVICES);
2509         class_unregister(&input_class);
2510 }
2511 
2512 subsys_initcall(input_init);
2513 module_exit(input_exit);

4.2 函数分析

4.2.1 class_register

函数原型

#define class_register(class) \

({ \

static struct lock_class_key __key; \

__class_register(class, &__key); \

})

int __class_register(struct class *cls, struct lock_class_key *key)

参数

struct class *cls

指向 class 结构体的指针,表示要注册的设备类

struct lock_class_key *key

指向 lock_class_key 结构体的指针,用于锁的调试和分类(通常用于锁的层次化管理)

返回值

int

成功:0 失败:错误码

功能

注册设备类

146 int __class_register(struct class *cls, struct lock_class_key *key)
147 {
148         struct subsys_private *cp;
149         int error;
150 
151         pr_debug("device class '%s': registering\n", cls->name);
152 
153         cp = kzalloc(sizeof(*cp), GFP_KERNEL);
154         if (!cp)
155                 return -ENOMEM;
156         klist_init(&cp->klist_devices, klist_class_dev_get, klist_class_dev_put);
157         INIT_LIST_HEAD(&cp->interfaces);
158         kset_init(&cp->glue_dirs);
159         __mutex_init(&cp->mutex, "subsys mutex", key);
160         error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);
161         if (error) {
162                 kfree(cp);
163                 return error;
164         }
165 
166         /* set the default /sys/dev directory for devices of this class */
167         if (!cls->dev_kobj)
168                 cls->dev_kobj = sysfs_dev_char_kobj;
169 
170 #if defined(CONFIG_BLOCK)
171         /* let the block class directory show up in the root of sysfs */
172         if (!sysfs_deprecated || cls != &block_class)
173                 cp->subsys.kobj.kset = class_kset;
174 #else    
175         cp->subsys.kobj.kset = class_kset;
176 #endif   
177         cp->subsys.kobj.ktype = &class_ktype;
178         cp->class = cls;
179         cls->p = cp;
180 
181         error = kset_register(&cp->subsys);
182         if (error) {
183                 kfree(cp);
184                 return error;
185         }                                 
186         error = class_add_groups(class_get(cls), cls->class_groups);
187         class_put(cls);
188         return error;
189 }

4.2.2 dev_name

函数原型

static inline const char *dev_name(const struct device *dev)

参数

const struct device *dev

指向 struct device 结构体的指针,该结构体表示一个设备实例

返回值

const char *

函数返回一个指向 const char 的指针,这个指针指向设备名称的字符串

功能

获取设备名称

1132 static inline const char *dev_name(const struct device *dev)
1133 {
1134         /* Use the init name until the kobject becomes available */
1135         if (dev->init_name)
1136                 return dev->init_name;
1137 
1138         return kobject_name(&dev->kobj);
1139 }

4.2.3 register_chrdev_region

函数原型

int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数

dev_t from

dev_t from: 起始设备号,指定从哪个设备号开始分配。

unsigned count

unsigned count: 设备号的数量,指定需要多少个连续的设备号

const char *name

const char *name: 设备的名称,通常用于在系统中标识这个设备

返回值

int

成功:字符设备分配设备号 失败:负值

功能

注册字符设备号

209 int register_chrdev_region(dev_t from, unsigned count, const char *name)
210 {
211         struct char_device_struct *cd;
212         dev_t to = from + count;
213         dev_t n, next;
214                 
215         for (n = from; n < to; n = next) {
216                 next = MKDEV(MAJOR(n)+1, 0);
217                 if (next > to)
218                         next = to;
219                 cd = __register_chrdev_region(MAJOR(n), MINOR(n),
220                                next - n, name);
221                 if (IS_ERR(cd))
222                         goto fail;
223         }
224         return 0;
225 fail:
226         to = n;
227         for (n = from; n < to; n = next) {
228                 next = MKDEV(MAJOR(n)+1, 0);
229                 kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
230         }
231         return PTR_ERR(cd);       
232 }

4.2.3 MKDEV

函数原型

#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

#define MINORBITS 20

参数

ma

主设备号

mi

此设备号

返回值

功能

将主设备号和次设备号组合成一个 dev_t 类型的设备号

5 示例

5.1 示例 1

5.1.1 代码

#include <linux/init.h> 
#include <linux/module.h> 
#include <linux/fs.h>
#include <linux/input.h>

struct input_dev *input_dev_s;

 static int test_init(void)
 {
     int ret;
     input_dev_s = input_allocate_device();
     input_dev_s->name = "input_test";
     input_dev_s->id.bustype = BUS_HOST;
     input_dev_s->id.vendor = 0x0001;
     input_dev_s->id.product = 0x0001;
     input_dev_s->id.version = 0x0100;

     ret = input_register_device(input_dev_s);
     if(ret)
	 {
		printk(KERN_ERR "register input device error\n");
        input_free_device(input_dev_s);
		goto failed;
	 }
     printk("register input device ok\r\n");		
     return 0;
failed:
	return ret;
 }

 static void test_exit(void)
 {
     printk("unregister input device\r\n");
     input_unregister_device(input_dev_s);
 }

module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");

5.1.2 操作

        注册成功后,/dev/input/下多了一个event4 文件节点

console:/data # ls /dev/input/                                                 
event0  event1  event2  event3
console:/data # insmod input_test.ko                                           
[  261.075255] input: input_test as /devices/virtual/input/input5
[  261.077768] register input device ok
console:/data # ls /dev/input/                                                 
event0  event1  event2  event3  event4

        使用 cat /proc/bus/input/devices 命令查看输入设备信息,可以看到 input_test 的输入设备信息。

130|console:/data #  cat /proc/bus/input/devices
I: Bus=0019 Vendor=524b Product=0006 Version=0100
N: Name="ff420030.pwm"
P: Phys=gpio-keys/remotectl
S: Sysfs=/devices/platform/ff420030.pwm/input/input0
U: Uniq=
H: Handlers=event0 cpufreq dmcfreq 
B: PROP=0
B: EV=3
B: KEY=70010 20000000000000 0 100010002000000 78000004000a800 1e16c000000000 10004ffc

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="adc_keys"
P: Phys=adc-keys/input0
S: Sysfs=/devices/platform/adc_keys/input/input1
U: Uniq=
H: Handlers=event1 cpufreq dmcfreq 
B: PROP=0
B: EV=3
B: KEY=c000000000000 0

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="gpio-keys"
P: Phys=gpio-keys/input0
S: Sysfs=/devices/platform/gpio-keys/input/input2
U: Uniq=
H: Handlers=event2 cpufreq dmcfreq 
B: PROP=0
B: EV=100003
B: KEY=10000000000000 0

I: Bus=0000 Vendor=0001 Product=0001 Version=0100
N: Name="rk-headset"
P: Phys=
S: Sysfs=/devices/platform/rk-headset/input/input3
U: Uniq=
H: Handlers=event3 
B: PROP=0
B: EV=3
B: KEY=400000000 0 0 0

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="input_test"
P: Phys=
S: Sysfs=/devices/virtual/input/input5
U: Uniq=
H: Handlers=event4 
B: PROP=0
B: EV=1

console:/data # 

5.2 mcu_cec驱动分析

5.2.1 注册输入事件

657 static int  mcu_cec_probe(struct i2c_client *client, const struct i2c_device_id *id)
658 {
659         int ret = 0;
660         int i;
661         struct mcu_cec *mcu_cec;
662         struct input_dev *input;
663         struct device_node *np = client->dev.of_node;
664         printk("%s: probe\n", __FUNCTION__);
665 
666 
667         mcu_cec = devm_kzalloc(&client->dev, sizeof(struct mcu_cec), GFP_KERNEL);
668         if (!mcu_cec)
669                 return -ENOMEM;
705         mcu_cec->mcu_cec_wq = create_singlethread_workqueue("mcu_cec_wq");
706         if (!mcu_cec->mcu_cec_wq){
707                 printk(KERN_ERR"%s: create workqueue failed\n", __func__);
708                 ret = -ENOMEM;
709                 goto failed;
710         }
711 
728         input = devm_input_allocate_device(&client->dev);
729         if (!input) {
730                 ret = -ENOMEM;
731                 goto failed;
732         }
733 
736         input->name = client->name;
737         input->phys = "cec-keys/input0";
738         input->dev.parent = &client->dev;
739 
740         input->id.bustype = BUS_HOST;
741         input->id.vendor = 0x0001;
742         input->id.product = 0x0001;
743         input->id.version = 0x0100;
744 
745         mcu_cec->input_dev = input;
746 
747         for (i=0;i<sizeof(mcu_cec_input_key)/sizeof(struct mcu_cec_key_table);i++){
748                         unsigned int type = EV_KEY;
749 
750                 input_set_capability(input, type, mcu_cec_input_key[i].keyCode);
751         }
752         input_set_capability(input, EV_KEY, KEY_WAKEUP);
753         input_set_capability(input, EV_KEY, KEY_F12);
754         ret = input_register_device(input);
755         if (ret) {
756                 input_free_device(input);
757                 goto failed;
758         }
760         mcu_cec->nb.notifier_call = cec_hotplug_notifier_call;
761         ret = cec_hotplug_reg_notifier(&mcu_cec->nb);
762         if (ret) {
763                 printk("failed to reg notifier: %d\n", ret);
764         }
765 
766         mutex_init(&mcu_cec->m_lock);
767         wake_lock_init(&mcu_cec->w_lock, WAKE_LOCK_SUSPEND, "mcu_lock");
768         wake_lock(&mcu_cec->w_lock);
769         printk("%s: probe ok!!\n", __FUNCTION__);
770     return 0;
771 failed:
772         return ret;
773 }

5.2.2 函数分析

5.2.2.1 devm_input_allocate_device

函数原型

struct input_dev *devm_input_allocate_device(struct device *dev)

参数

struct device *dev

设备结构体指针

返回值

struct input_dev *

输入设备结构体指针

功能

分配并初始化一个输入设备

1862 struct input_dev *devm_input_allocate_device(struct device *dev)
1863 {
1864         struct input_dev *input;
1865         struct input_devres *devres;
1866 
1867         devres = devres_alloc(devm_input_device_release,
1868                               sizeof(*devres), GFP_KERNEL);
1869         if (!devres)
1870                 return NULL;
1871 
1872         input = input_allocate_device();
1873         if (!input) {
1874                 devres_free(devres);
1875                 return NULL;
1876         }
1877 
1878         input->dev.parent = dev;
1879         input->devres_managed = true; 
1880 
1881         devres->input = input;
1882         devres_add(dev, devres);
1883 
1884         return input;
1885 }
1834 static void devm_input_device_release(struct device *dev, void *res)
1835 {
1836         struct input_devres *devres = res;
1837         struct input_dev *input = devres->input;
1838 
1839         dev_dbg(dev, "%s: dropping reference to %s\n",
1840                 __func__, dev_name(&input->dev));
1841         input_put_device(input);
1842 }

devm_input_allocate_device 函数提供了一个方便的方式来分配和初始化 input_dev 设备,并利用设备管理机制自动处理资源释放。这使得设备的创建和销毁变得更加简洁和安全。

devres 机制: devres(设备资源)用于管理与设备相关的资源。devres_add 将资源添加到设备的资源管理列表中,devm_input_device_release 函数会在设备释放时调用,以释放相关资源。

自动资源管理: 通过 devres 机制,确保在设备卸载时,input_dev 设备会被正确释放,避免内存泄漏和资源浪费。

父设备设置: 将 input_dev 的父设备设置为传入的设备 dev,确保设备之间的关系正确设置,有利于设备树的管理和资源的分配。

5.2.3 上报事件

90 static struct mcu_cec_key_table mcu_cec_input_key[] = {
 91     {0x40, KEY_POWER},     //power off 
 92     {0x30, KEY_POWER},     //power off 
 93     {0x01, KEY_UP},
 94     {0x02, KEY_DOWN},
 95     {0x03, KEY_LEFT},
 96     {0x04, KEY_RIGHT},  
 97     {0x41, KEY_VOLUMEUP},
 98     {0x42, KEY_VOLUMEDOWN},
 99     {0x43, KEY_BACK},       //mute
100     {0x2b, KEY_ENTER},
101     {0x20, KEY_0},
102     {0x21, KEY_1},
103     {0x22, KEY_2},
104     {0x23, KEY_3},
105     {0x24, KEY_4},
106     {0x25, KEY_5},
107     {0x26, KEY_6},
108     {0x27, KEY_7},
109     {0x28, KEY_8},
110     {0x29, KEY_9},
111     {0x2a, KEY_DOT},
112     {0xd, KEY_BACK},
113     {0x48, KEY_REWIND},
114     {0x46, KEY_PLAYPAUSE},
115     {0x49, KEY_FASTFORWARD},
116     {0x44, KEY_PLAYPAUSE},
117     {0x45, KEY_STOP},
118     {0x71, KEY_MENU},//KEY_BLUE
119     {0x74, KEY_HOME},//KEY_YELLOW KEY_HOME
120     {0xa, KEY_MENU},//KEY_BLUE
121     {0x09, KEY_HOME},//KEY_YELLOW KEY_HOME
122     {0x4b, KEY_NEXTSONG},
123     {0x4c, KEY_PREVIOUSSONG},
124 };

127 static unsigned char cec_get_keycode(unsigned char cecCode)
128 {
129     int i;
130 
131     for (i = 0; i < sizeof(mcu_cec_input_key)/sizeof(struct mcu_cec_key_table); i++){
132         if (mcu_cec_input_key[i].cecCode == cecCode){
133             return mcu_cec_input_key[i].keyCode;
134         }
135     }
136         return 0;
137 }

426 static void mcu_cec_report_standby_task(struct work_struct *work)
427 {
428     int ret;
429     unsigned char ceckey;
430     unsigned char keyCode;
431     cec_int_status mcu_value;
432     struct mcu_cec *mcu_cec = container_of(work, struct mcu_cec, work);
433     mutex_lock(&mcu_cec->m_lock);
434     ret = regmap_read(mcu_cec->regmap, CEC_INT_STATUS, (int *)&mcu_value.status);
435     if (ret) {
436           dev_err(&mcu_cec->i2c->dev, "read 0x%x failed\n", CEC_INT_STATUS);
437           goto failed;
438     }
439     if(mcu_value.status_bit.req_standby == 1){
440          mcu_value.status_bit.req_standby = 0;
441          ret = regmap_write(mcu_cec->regmap, CEC_INT_STATUS, (unsigned int)mcu_value.status);
442          if (ret) {
443                 dev_err(&mcu_cec->i2c->dev, "write 0x%x failed\n", CEC_INT_STATUS);
444          }
445          input_event(mcu_cec->input_dev, EV_KEY, KEY_F12, 1);
446          input_sync(mcu_cec->input_dev);
447          input_event(mcu_cec->input_dev, EV_KEY, KEY_F12, 0);//KEY_WAKEUP
448          input_sync(mcu_cec->input_dev);
449          pr_err("report host stanby\n");
450     }else if(mcu_value.status_bit.remote_control == 1){
451          mcu_value.status_bit.remote_control = 0;
452          ret = regmap_write(mcu_cec->regmap, CEC_INT_STATUS, (unsigned int)mcu_value.status);
453          if (ret) {
454                 dev_err(&mcu_cec->i2c->dev, "write 0x%x failed\n", CEC_INT_STATUS);
455          }
456 
457         ret = regmap_read(mcu_cec->regmap, CEC_REMOTE_KEY, (unsigned int *)&ceckey);
458         if (ret) {
459           dev_err(&mcu_cec->i2c->dev, "read 0x%x failed\n", CEC_INT_STATUS);
460           goto failed;
461         }
462 
463         keyCode = cec_get_keycode(ceckey);
464 
465         {
466                 mcu_cec->keycode = keyCode;
467                 if (mcu_cec->keycode == KEY_POWER)
468                 {
469                         if(mcu_cec->input_dev){
470                                 input_event(mcu_cec->input_dev, EV_KEY, KEY_POWER, 1);
471                                 input_sync(mcu_cec->input_dev);
472                                 input_event(mcu_cec->input_dev, EV_KEY, KEY_POWER, 0);//KEY_WAKEUP
473                                 input_sync(mcu_cec->input_dev);
474                         }
475                 }else {
476                         input_event(mcu_cec->input_dev, EV_KEY, mcu_cec->keycode, 1);
477                         input_sync(mcu_cec->input_dev);
478                         input_event(mcu_cec->input_dev, EV_KEY, mcu_cec->keycode, 0);
479                         input_sync(mcu_cec->input_dev);
480                 }
481         }
482 
483 
484         // report_cec_input_keycode(mcu_cec);
485     }
486     enable_irq(mcu_cec->i2c->irq);
487 failed:
488     mutex_unlock(&mcu_cec->m_lock);
489 
490 }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2181826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用网络分析仪测试功分器驻波的5个步骤

在射频系统中&#xff0c;功分器的驻波比直接关系到信号的稳定性和传输效率。本文将带您深入了解驻波比的测试方法和影响其结果的因素。 一、功分器驻波比 驻波(Voltage Standing Wave Ratio)&#xff0c;简称SWR或VSWR&#xff0c;是指频率相同、传输方向相反的两种波&#xf…

.NET Core 高性能并发编程

一、高性能大并发架构设计 .NET Core 是一个高性能、可扩展的开发框架&#xff0c;可以用于构建各种类型的应用程序&#xff0c;包括高性能大并发应用程序。为了设计和开发高性能大并发 .NET Core 应用程序&#xff0c;需要考虑以下几个方面&#xff1a; 1. 异步编程 异步编程…

最大正方形 Python题解

最大正方形 题目描述 在一个 n m n\times m nm 的只包含 0 0 0 和 1 1 1 的矩阵里找出一个不包含 0 0 0 的最大正方形&#xff0c;输出边长。 输入格式 输入文件第一行为两个整数 n , m ( 1 ≤ n , m ≤ 100 ) n,m(1\leq n,m\leq 100) n,m(1≤n,m≤100)&#xff0c;接…

养猪场饲料加工机械设备有哪些

养猪场饲料加工机械设备主要包括以下几类&#xff1a;1‌、粉碎机‌&#xff1a;主要用于将原料进行粉碎&#xff0c;以便与其他饲料原料混合均匀。常见的粉碎机有水滴式粉碎机和立式粉碎机两种&#xff0c;用户可以根据原料的特性选择适合的机型。2‌、搅拌机‌&#xff1a;用…

ONVIF、GB28181技术特点和使用场景分析

技术背景 好多开发者希望搞明白ONVIF和GB28181的区别和各自适合的场景&#xff0c;为什么大牛直播SDK只做了GB28181接入端&#xff0c;没有做ONVIF&#xff1f;本文就二者差别&#xff0c;做个大概的介绍。 ONVIF ONVIF&#xff08;Open Network Video Interface Forum&…

【Linux 23】线程池

文章目录 &#x1f308; 一、线程池的概念&#x1f308; 二、线程池的应用场景&#x1f308; 三、线程池的实现 &#x1f308; 一、线程池的概念 线程池 (thread pool) 是一种利用池化技术的线程使用模式。 虽然创建线程的代价比创建进程的要小很多&#xff0c;但小并不意味着…

Mysql高级篇(下)——日志

日志 一、日志概述二、日志弊端二、日志分类三、 各日志详情介绍1、慢查询日志&#xff08;Slow Query Log&#xff09;2、通用查询日志&#xff08;General Query Log&#xff09;3、错误日志&#xff08;Error Log&#xff09;4、二进制日志&#xff08;Binary Log&#xff0…

初识Linux · 进程等待

目录 前言&#xff1a; 进程等待是什么 为什么需要进程等待 进程等待都在做什么 前言&#xff1a; 通过上文的学习&#xff0c;我们了解了进程终止&#xff0c;知道终止是在干什么&#xff0c;终止的三种情况&#xff0c;以及有了退出码&#xff0c;错误码的概念&#xff…

基于大数据的学生体质健康信息系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

图像数据增强albumentations之自然景色

一 背景 最近在做关于图像数据增强方面&#xff0c;发现albumentations这个包比较好用&#xff0c;在此学习一下如何使用API二 albumentations 安装 注意&#xff0c;注意&#xff0c;注意 python版本3.8 pip install -U albumentations三 API学习 1 模拟雨水 import os i…

慢病中医药膳养生食疗管理微信小程序、基于微信小程序的慢病中医药膳养生食疗管理系统设计与实现、中医药膳养生食疗管理微信小程序的开发与应用(源码+文档+定制)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

【SpringCloud】注册中⼼的其他实现-Nacos

Nacos 1. Nacos简介 2. Nacos安装2.1 下载安装包2.2 Windows2.2.1 解压2.2.2 修改单机模式2.2.3 启动Nacos2.2.4 常⻅问题集群模式启动端⼝号冲突 2.3 Linux2.3.1 准备安装包2.3.2 单机模式启动 1. Nacos简介 2018年6⽉, Eureka 2.0宣布闭源(但是1.X版本仍然为活跃项⽬), 同年…

【mmengine】配置器(config)(进阶)继承与导出,命令行修改配置

一、配置文件的继承 1.1 继承机制概述 新建optimizer_cfg.py: optimizer dict(typeSGD, lr0.02, momentum0.9, weight_decay0.0001)新建runtime_cfg.py: device "cuda" gpu_ids [0, 1] batch_size 64 epochs 100 num_workers 8新建resnet50.py: _base_ […

图解C#高级教程(三):泛型

本讲用许多代码示例介绍了 C# 语言当中的泛型&#xff0c;主要包括泛型类、接口、结构、委托和方法。 文章目录 1. 为什么需要泛型&#xff1f;2. 泛型类的定义2.1 泛型类的定义2.2 使用泛型类创建变量和实例 3. 使用泛型类实现一个简单的栈3.1 类型参数的约束3.2 Where 子句3…

不相同的二叉搜索树

给你一个整数 n &#xff0c;求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种&#xff1f;返回满足题意的二叉搜索树的种数。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;5示例 2&#xff1a; 输入&#xff1a;n 1 输出&#xff1a;1提…

数字教学时代:构建高效在线帮助中心的重要性

在数字化教学日益普及的今天&#xff0c;教育领域正经历着前所未有的变革。随着在线课程、虚拟教室、智能学习平台等数字化工具的广泛应用&#xff0c;教育资源的获取方式和学习模式发生了深刻变化。然而&#xff0c;这种变革也带来了新的挑战&#xff0c;其中之一便是如何确保…

YashanDB Docker镜像制作

本文作者&#xff1a;YashanDB中级服务工程师鲍健昕 为什么需要Docker部署数据库 常规使用 yasboot 部署数据库的方法&#xff0c;操作流程复杂&#xff0c;需要配置许多配置文件以及环境变量&#xff0c;不同用户使用的环境不同&#xff0c;那么环境配置也会存在差异&#x…

YOLO11震撼发布!

非常高兴地向大家介绍 Ultralytics YOLO系列的新模型&#xff1a; YOLO11&#xff01; YOLO11 在以往 YOLO 模型基础上带来了一系列强大的功能和优化&#xff0c;使其速度更快、更准确、用途更广泛。主要改进包括 增强了特征提取功能&#xff0c;从而可以更精确地捕捉细节以更…

啤酒在文学中的浪漫形象:精酿啤酒的诗意之旅

在文学的浩瀚星空中&#xff0c;啤酒并非仅仅是醉人的琼浆&#xff0c;它更是一种情感的载体&#xff0c;一种浪漫的符号。尤其是当提及Fendi Club精酿啤酒时&#xff0c;我们仿佛能闻到那从古老酒窖中飘出的馥郁香气&#xff0c;感受到它在文字间流淌的诗意与温情。 一、啤酒…

uniapp中检测应用更新的两种方式-升级中心之uni-upgrade-center-app

uniapp一个很是用的功能&#xff0c;就是在我们发布新版本的app后&#xff0c;需要提示用户进行app更新&#xff0c;并告知用户我们新版的app更新信息&#xff0c;以使得用户能及时使用上我们新开发的功能&#xff0c;提升用户的实用度和粘性。注意:这个功能只能在app端使用 效…