三星framebuffer驱动代码分析

一、驱动总体概述

本次的驱动代码是Samsung公司为s5pv210这款SoC编写的framebuffer驱动,对应于s5pv210中的内部外设Display Controller (FIMD)模块。

驱动代码是基于platform平台总线编写的。

1、驱动代码的源文件分布:

   (1):drivers/video/samsung/s3cfb.c,  驱动主体
   (2):drivers/video/samsung/s3cfb_fimd6x.c,里面有很多LCD硬件操作的函数
   (3):arch/arm/mach-s5pv210/mach-x210.c,负责提供platform_device,这个文件里面提供了很多的基于platform总线编写的驱动需要的platform_device。

          mach文件是每一个移植好的内核都会提供这个文件的,例如这里的mach-x210.c文件是开发板厂商从三星提供的mach文件移植而来的。
   (4):arch/arm/plat-s5p/devs.c,为platform_device提供一些硬件描述信息

2、当我们接触到一种新的驱动框架的时候,怎么能够找到驱动框架源代码(入口函数)所在哪个源文件中?

    (1):经验:靠经验的前提是你之前就已经接触过很多的驱动框架,你能够靠你的经验大概猜出来是哪些文件

    (2):可以分析内核源码树中menuconfig、Makefile、Kconfig等

    (3):内核编译后检查编译结果中的.o文件

二、platform_driver平台设备驱动部分

1、注册/卸载平台驱动:s3cfb_register/s3cfb_unregister   (driversvideosamsungs3cfb.c)

(1)platform_driver结构体变量s3cfb_driver

1 static struct platform_driver s3cfb_driver = {
2     .probe = s3cfb_probe,                             //  平台的probe函数
3     .remove = __devexit_p(s3cfb_remove),
4     .driver = {
5            .name = S3CFB_NAME,                  //  平台设备驱动的名字    s3cfb
6            .owner = THIS_MODULE,
7     },
8 };

2、相关的数据结构

 1 struct s3c_platform_fb {
 2     int        hw_ver;
 3     char        clk_name[16];
 4     int        nr_wins;                    //  这个表示虚拟窗口的数量
 5     int        nr_buffers[5];
 6     int        default_win;             //  这个表示当前默认的窗口
 7     int        swap;
 8     phys_addr_t    pmem_start; /* starting physical address of memory region */  //   显存的物理起始地址
 9     size_t        pmem_size; /* size of memory region */                                     //  显存的字节大小
10     void            *lcd;
11     void        (*cfg_gpio)(struct platform_device *dev);                                     //  LCD相关gpio的配置
12     int        (*backlight_on)(struct platform_device *dev);                              //  打开LCD的背光
13     int        (*backlight_onoff)(struct platform_device *dev, int onoff);          //  关闭LCD的背光
14     int        (*reset_lcd)(struct platform_device *dev);                                     //  复位LCD
15     int        (*clk_on)(struct platform_device *pdev, struct clk **s3cfb_clk);    //  LCD相关的时钟打开
16     int        (*clk_off)(struct platform_device *pdev, struct clk **clk);              //  LCD相关的时钟关闭
17 };
 1 struct s3cfb_global {
 2     /* general */
 3     void __iomem        *regs;                    //  SoC中LCD控制器部分相关的寄存器地址的基地址(虚拟地址)   Display Controller (FIMD)模块
 4     struct mutex        lock;                     //   互斥锁
 5     struct device        *dev;                     //  表示本fb设备的device指针
 6     struct clk        *clock;
 7     struct regulator    *regulator;
 8     int            irq;                               //  本LCD使用到的中断号
 9     struct fb_info        **fb;                     //   fb_info 的二重指针   用来指向一个 fb_info 指针数组  
10     struct completion    fb_complete;
11 
12     /* fimd */
13     int            enabled;
14     int            dsi;
15     int            interlace;
16     enum s3cfb_output_t    output;           //  LCD的输出模式
17     enum s3cfb_rgb_mode_t    rgb_mode;     //  RGB色彩模式 
18     struct s3cfb_lcd    *lcd;                       //  用来描述一个LCD的硬件信息
19 
20 #ifdef CONFIG_HAS_WAKELOCK
21     struct early_suspend    early_suspend;
22     struct wake_lock    idle_lock;
23 #endif
24 
25 #ifdef CONFIG_CPU_FREQ
26     struct notifier_block    freq_transition;
27     struct notifier_block    freq_policy;
28 #endif
29 
30 };
 1 struct s3cfb_lcd {
 2     int    width;           //  水平像素
 3     int    height;         //   垂直像素
 4     int    p_width;       //   物理宽度 mm
 5     int    p_height;     //   物理高度mm
 6     int    bpp;            //   像素深度
 7     int    freq;            //    LCD的刷新率
 8     struct    s3cfb_lcd_timing timing;        //  LCD时序相关的参数
 9     struct    s3cfb_lcd_polarity polarity;   //   这个是用来表示LCD的各种电平信号是否需要进行翻转
10 
11     void    (*init_ldi)(void);       //  用来初始化 LDI    我不知道LDI是什么东西
12     void    (*deinit_ldi)(void);
13 };

3、函数详解

(1)s3cfb_probe函数分析:

  1 static int __devinit s3cfb_probe(struct platform_device *pdev)
  2 {
  3     struct s3c_platform_fb *pdata;              //  这个是三星封装的一个用来表示平台设备层的私有数据的结构体
  4     struct s3cfb_global *fbdev;                   //  设备驱动部分封装的一个全局的结构体,这个结构体主要作用是在驱动部分的2个文件(s3cfb.c和s3cfb_fimd6x.c)的函数中做数据传递用的
  5     struct resource *res;                              //  定义一个资源结构体指针
  6     int i, j, ret = 0;
  7 
  8     fbdev = kzalloc(sizeof(struct s3cfb_global), GFP_KERNEL);    //  给 fpdev 申请分配内存
  9     if (!fbdev) {
 10         dev_err(&pdev->dev, "failed to allocate for "
 11             "global fb structure
");
 12         ret = -ENOMEM;
 13         goto err_global;
 14     }
 15     fbdev->dev = &pdev->dev;              //  通过 fbdev->dev  指向 pdev->dev       /sys/devices/platform/s3cfb/   这个目录作为fb设备的父设备目录
 16 
 17     fbdev->regulator = regulator_get(&pdev->dev, "pd");  //  调整器 : 动态电流和电压控制,具体的我也不清楚
 18     if (!fbdev->regulator) {
 19         dev_err(fbdev->dev, "failed to get regulator
");
 20         ret = -EINVAL;
 21         goto err_regulator;
 22     }
 23     ret = regulator_enable(fbdev->regulator);
 24     if (ret < 0) {
 25         dev_err(fbdev->dev, "failed to enable regulator
");
 26         ret = -EINVAL;
 27         goto err_regulator;
 28     }
 29     pdata = to_fb_plat(&pdev->dev);   //  获取平台设备层的私有数据   pdev->dev-> platform_data  存放在 pdata中         
 30     if (!pdata) {
 31         dev_err(fbdev->dev, "failed to get platform data
");
 32         ret = -EINVAL;
 33         goto err_pdata;
 34     }
 35 
 36     fbdev->lcd = (struct s3cfb_lcd *)pdata->lcd;     //  通过fbdev->lcd 指向 pdata->lcd    
 37    
 38     if (pdata->cfg_gpio)   //  如果平台设备的私有数据中的cfg_gpio指向了一个有效的配置LCD相关的gpio的方法
 39         pdata->cfg_gpio(pdev);   //  则调用这个函数
 40 
 41     if (pdata->clk_on)     //   打开LCD相关的时钟设置
 42         pdata->clk_on(pdev, &fbdev->clock);
 43 
 44     res = platform_get_resource(pdev, IORESOURCE_MEM, 0);   //  获取平台设备的IO资源
 45     if (!res) {
 46         dev_err(fbdev->dev, "failed to get io memory region
");
 47         ret = -EINVAL;
 48         goto err_io;
 49     }
 50 
 51     res = request_mem_region(res->start,      //      请求进行物理地址到虚拟地址的映射
 52                  res->end - res->start + 1, pdev->name);
 53     if (!res) {
 54         dev_err(fbdev->dev, "failed to request io memory region
");
 55         ret = -EINVAL;
 56         goto err_io;
 57     }
 58 
 59     fbdev->regs = ioremap(res->start, res->end - res->start + 1);   //   申请物理地址到虚拟地址的映射,将映射得到的虚拟地址存放在 fbdev->regs
 60     if (!fbdev->regs) {
 61         dev_err(fbdev->dev, "failed to remap io region
");
 62         ret = -EINVAL;
 63         goto err_mem;
 64     }
 65 
 66     s3cfb_set_vsync_interrupt(fbdev, 1);            //   使能vsync中断(场同步信号中断)
 67     s3cfb_set_global_interrupt(fbdev, 1);          //    全局中断使能: 使能视频帧中断 和 使能视频中断
 68     s3cfb_init_global(fbdev);                             //   全局初始化
 69 
 70     if (s3cfb_alloc_framebuffer(fbdev)) {    //   给fb_info 申请分配内存  并构建fb_info结构体
 71         ret = -ENOMEM;
 72         goto err_alloc;
 73     }
 74 
 75     if (s3cfb_register_framebuffer(fbdev)) {    //  注册fb设备    内部其实就是调用了FB驱动框架层中 register_framebuffer 函数进行注册
 76         ret = -EINVAL;
 77         goto err_register;
 78     }
 79 
 80     s3cfb_set_clock(fbdev);                        //   时钟设置
 81     s3cfb_set_window(fbdev, pdata->default_win, 1);        //   虚拟窗口相关的设置
 82 
 83     s3cfb_display_on(fbdev);                    //  打开LCD显示
 84 
 85     fbdev->irq = platform_get_irq(pdev, 0);            //   获取平台设备私有数据中的 中断号资源
 86     if (request_irq(fbdev->irq, s3cfb_irq_frame, IRQF_SHARED,      //   申请中断
 87             pdev->name, fbdev)) {
 88         dev_err(fbdev->dev, "request_irq failed
");
 89         ret = -EINVAL;
 90         goto err_irq;
 91     }
 92 
 93 #ifdef CONFIG_FB_S3C_LCD_INIT
 94     if (pdata->backlight_on)
 95         pdata->backlight_on(pdev);      
 96 
 97     if (!bootloaderfb && pdata->reset_lcd)  
 98         pdata->reset_lcd(pdev);
 99 #endif
100 
101 #ifdef CONFIG_HAS_EARLYSUSPEND
102     fbdev->early_suspend.suspend = s3cfb_early_suspend;
103     fbdev->early_suspend.resume = s3cfb_late_resume;
104     fbdev->early_suspend.level = EARLY_SUSPEND_LEVEL_DISABLE_FB;
105     register_early_suspend(&fbdev->early_suspend);
106 #endif
107 
108     ret = device_create_file(&(pdev->dev), &dev_attr_win_power);     //    在平台设备下   /sys/devices/platform/pdev_dev/dev_attr_win_power   属性文件
109     if (ret < 0)                                                                                          //    pdev_dev表示的就是我们的平台设备的名字
110         dev_err(fbdev->dev, "failed to add sysfs entries
");
111 
112     dev_info(fbdev->dev, "registered successfully
");
113 
114 #if !defined(CONFIG_FRAMEBUFFER_CONSOLE) && defined(CONFIG_LOGO)        //   下面这个是处理Linux启动logo 相关的代码
115     if (fb_prepare_logo( fbdev->fb[pdata->default_win], FB_ROTATE_UR)) {
116         printk("Start display and show logo
");
117         /* Start display and show logo on boot */
118         fb_set_cmap(&fbdev->fb[pdata->default_win]->cmap, fbdev->fb[pdata->default_win]);
119         fb_show_logo(fbdev->fb[pdata->default_win], FB_ROTATE_UR);
120     }
121 #endif
122     mdelay(100);
123     if (pdata->backlight_on)                        //  打开背光
124         pdata->backlight_on(pdev);
125 
126     return 0;
127 
128 err_irq:
129     s3cfb_display_off(fbdev);
130     s3cfb_set_window(fbdev, pdata->default_win, 0);
131     for (i = pdata->default_win;
132             i < pdata->nr_wins + pdata->default_win; i++) {
133         j = i % pdata->nr_wins;
134         unregister_framebuffer(fbdev->fb[j]);
135     }
136 err_register:
137     for (i = 0; i < pdata->nr_wins; i++) {
138         if (i == pdata->default_win)
139             s3cfb_unmap_default_video_memory(fbdev->fb[i]);
140         framebuffer_release(fbdev->fb[i]);
141     }
142     kfree(fbdev->fb);
143 
144 err_alloc:
145     iounmap(fbdev->regs);
146 
147 err_mem:
148     release_mem_region(res->start,
149                  res->end - res->start + 1);
150 
151 err_io:
152     pdata->clk_off(pdev, &fbdev->clock);
153 
154 err_pdata:
155     regulator_disable(fbdev->regulator);
156 
157 err_regulator:
158     kfree(fbdev);
159 
160 err_global:
161     return ret;
162 }

(2)s3cfb_init_global

 1 static int s3cfb_init_global(struct s3cfb_global *ctrl)
 2 {
 3     ctrl->output = OUTPUT_RGB;                     //  设置初始模式
 4     ctrl->rgb_mode = MODE_RGB_P;               //   设置RGB色彩模式
 5 
 6     init_completion(&ctrl->fb_complete);       //  初始化完成量(注: 完成量也是一种内核提供的同步机制)
 7     mutex_init(&ctrl->lock);
 8 
 9     s3cfb_set_output(ctrl);              //  寄存器配置LCD的输出模式
10     s3cfb_set_display_mode(ctrl);  //   寄存器配置LCD的显示模式
11     s3cfb_set_polarity(ctrl);            //   寄存器配置信号电平翻转
12     s3cfb_set_timing(ctrl);              //   寄存器配置LCD时序参数
13     s3cfb_set_lcd_size(ctrl);            //   寄存器配置LCD的水平、垂直像素大小
14 
15     return 0;
16 }

(3)s3cfb_alloc_framebuffer

  1 static int s3cfb_alloc_framebuffer(struct s3cfb_global *ctrl)
  2 {
  3     struct s3c_platform_fb *pdata = to_fb_plat(ctrl->dev);     //  通过 ctrl->dev 去获取平台设备的私有数据
  4     int ret, i;
  5 
  6     ctrl->fb = kmalloc(pdata->nr_wins *             //   给ctrl->fb  的这个fb_info指针数组分配内存
  7             sizeof(*(ctrl->fb)), GFP_KERNEL);    //   数量  nr_wins
  8     if (!ctrl->fb) {
  9         dev_err(ctrl->dev, "not enough memory
");
 10         ret = -ENOMEM;
 11         goto err_alloc;
 12     }
 13 
 14     for (i = 0; i < pdata->nr_wins; i++) {                //  给fb_info 指针数组中的每一个指针申请分配内存
 15         ctrl->fb[i] = framebuffer_alloc(sizeof(*ctrl->fb),
 16                          ctrl->dev);
 17         if (!ctrl->fb[i]) {
 18             dev_err(ctrl->dev, "not enough memory
");
 19             ret = -ENOMEM;
 20             goto err_alloc_fb;
 21         }
 22 
 23         s3cfb_init_fbinfo(ctrl, i);    //  初始化fb_info  这个结构体   就是去构建fb_info
 24 
 25         if (i == pdata->default_win) {
 26             if (s3cfb_map_video_memory(ctrl->fb[i])) {        //  给FB显存确定内存地址和分配空间(注意只是对默认的fb设备分配了,一个虚拟的显示窗口其实就是抽象为一个fb设备,多个窗口其实是会进行叠加的)
 27                 dev_err(ctrl->dev,
 28                     "failed to map video memory "
 29                     "for default window (%d)
", i);
 30                 ret = -ENOMEM;
 31                 goto err_map_video_mem;
 32             }
 33         }
 34     }
 35 
 36     return 0;
 37 
 38 err_alloc_fb:
 39     while (--i >= 0) {
 40         if (i == pdata->default_win)
 41             s3cfb_unmap_default_video_memory(ctrl->fb[i]);
 42 
 43 err_map_video_mem:
 44         framebuffer_release(ctrl->fb[i]);
 45     }
 46     kfree(ctrl->fb);
 47 
 48 err_alloc:
 49     return ret;
 50 }
 51 
 52 
 53 
 54 struct fb_info *framebuffer_alloc(size_t size, struct device *dev)
 55 {
 56 #define BYTES_PER_LONG (BITS_PER_LONG/8)
 57 #define PADDING (BYTES_PER_LONG - (sizeof(struct fb_info) % BYTES_PER_LONG))
 58     int fb_info_size = sizeof(struct fb_info);          //  获取fb_info结构体类型的字节大小
 59     struct fb_info *info;                    
 60     char *p;
 61 
 62     if (size)
 63         fb_info_size += PADDING;            
 64 
 65     p = kzalloc(fb_info_size + size, GFP_KERNEL);
 66 
 67     if (!p)
 68         return NULL;
 69 
 70     info = (struct fb_info *) p;
 71 
 72     if (size)
 73         info->par = p + fb_info_size;
 74 
 75     info->device = dev;       //   指定我们的 fb 设备的父类设备是平台设备    /sys/devices/platform/plat_xxxdev/  这个目录,也就是我们将来创建的设备就在这个目录下 
 76 
 77 #ifdef CONFIG_FB_BACKLIGHT
 78     mutex_init(&info->bl_curve_mutex);
 79 #endif
 80  
 81     return info;
 82 #undef PADDING
 83 #undef BYTES_PER_LONG
 84 }
 85 
 86 
 87 
 88 static int s3cfb_map_video_memory(struct fb_info *fb)
 89 {
 90     struct fb_fix_screeninfo *fix = &fb->fix;
 91     struct s3cfb_window *win = fb->par;
 92     struct s3cfb_global *fbdev =
 93         platform_get_drvdata(to_platform_device(fb->device));
 94     struct s3c_platform_fb *pdata = to_fb_plat(fbdev->dev);
 95 
 96     if (win->owner == DMA_MEM_OTHER) {
 97         fix->smem_start = win->other_mem_addr;
 98         fix->smem_len = win->other_mem_size;
 99         return 0;
100     }
101 
102     if (fb->screen_base)          //  如果我们之前就已经确定了FB的显存地址的虚拟地址,那么就直接退出,因为这个函数的作用就是给显存确定虚拟内存地址并分配内存空间
103         return 0;
104 
105     if (pdata && pdata->pmem_start && (pdata->pmem_size >= fix->smem_len)) {  //  如果我们的平台设备中的私有数据中已经确定了显存的物理地址和大小
106         fix->smem_start = pdata->pmem_start;                                                     //  那么就使用平台设备私有数据中定义的
107         fb->screen_base = ioremap_wc(fix->smem_start, pdata->pmem_size);
108     } else
109         fb->screen_base = dma_alloc_writecombine(fbdev->dev,       //  否则的话我们就自己申请分配显存空间
110                          PAGE_ALIGN(fix->smem_len),
111                          (unsigned int *)
112                          &fix->smem_start, GFP_KERNEL);
113 
114     if (!fb->screen_base)
115         return -ENOMEM;
116 
117     dev_info(fbdev->dev, "[fb%d] dma: 0x%08x, cpu: 0x%08x, "
118              "size: 0x%08x
", win->id,
119              (unsigned int)fix->smem_start,
120              (unsigned int)fb->screen_base, fix->smem_len);
121 
122     memset(fb->screen_base, 0, fix->smem_len);                 //  将FB显存清零
123     win->owner = DMA_MEM_FIMD;
124 
125     return 0;
126 }

三、platform_device平台设备部分

fb的驱动是基于platform平台总线的,所以需要提供platform_device(注册平台设备)和platform_driver(注册平台驱动)。前面讲的是平台驱动部分

那么它对应的平台设备的注册在什么地方呢? 答案就是之前说的mach文件中,我这里是 archarmmach-s5pv210mach-x210.c 这个文件。

之前说了,这个文件中注册了很多的系统中可能用到的平台设备,将来写驱动的时候,只需要注册平台驱动即可,当然如果没有,可能就需要自己去添加。

这个文件中将所有的平台设备结构体都放在一个 struct platform_device *类型的数组smdkc110_devices中,将所有定义好的platform_device结构体挂接到这个数组中去,

在 smdkc110_machine_init 函数中将所有的平台设备都进行了注册。  如下: smdkc110_machine_init 这个函数其实是被链接在Linux启动的各个初始化段中的某一个,所以

当系统启动的时候,执行了初始化段中的函数时,smdkc110_machine_init 函数就会被调用。

smdkc110_machine_init

    platform_add_devices(smdkc110_devices, ARRAY_SIZE(smdkc110_devices));        //   平台设备的注册

    s3cfb_set_platdata(&ek070tn93_fb_data);                                                         //   给平台设备设置私有数据

1、struct  platform_device  s3c_device_fb变量

s3c_device_fb 是fb的平台总线驱动下提供的 platform_device 类型变量,这个变量定义在:archarmplat-s5pdevs.c 文件中

 1 struct platform_device s3c_device_fb = {
 2     .name          = "s3cfb",                                            //  平台设备的名字
 3     .id          = -1, 
 4     .num_resources      = ARRAY_SIZE(s3cfb_resource),  //  平台设备的资源数量
 5     .resource      = s3cfb_resource,                                        //  平台设备的资源
 6     .dev          = {
 7         .dma_mask        = &fb_dma_mask,
 8         .coherent_dma_mask    = 0xffffffffUL
 9     }
10 };

(1)从定义的变量中可以看出来,并没有挂接设备的私有数据到s3c_device_fb变量中,因为platform_device结构体中device结构体下的platform_data指针并没有被赋值

那么是不是这个平台设备没有私有数据呢?

答案是肯定有的,因为前面在分析平台驱动部分时都使用了平台设备的私有数据,那么之前说过,数据有使用的地方,肯定是有产生数据的地方,一定要弄清楚这么一个关系。

那么数据的产生地在那呢?  其实就是在smdkc110_machine_init函数中,这个函数中通过调用另一个函数(s3cfb_set_platdata)来挂接fb平台设备的私有数据。

s3cfb_set_platdata(&ek070tn93_fb_data);

 1 static struct s3c_platform_fb ek070tn93_fb_data __initdata = {
 2     .hw_ver    = 0x62,
 3     .nr_wins = 5,
 4     .default_win = CONFIG_FB_S3C_DEFAULT_WINDOW,    //  默认开启的虚拟窗口
 5     .swap = FB_SWAP_WORD | FB_SWAP_HWORD,
 6  
 7     .lcd = &ek070tn93,                                              //  描述LCD硬件信息的结构体
 8     .cfg_gpio    = ek070tn93_cfg_gpio,                         //  配置LCD相关的gpio的方法
 9     .backlight_on    = ek070tn93_backlight_on,          //   使能LCD背光
10     .backlight_onoff    = ek070tn93_backlight_off,   //   关闭LCD背光
11     .reset_lcd    = ek070tn93_reset_lcd,                        //   复位LCD
12 }; 

当我们要去移植一款LCD时,一般只需要对这个结构体里面的内容进行的更改,例如 gpio、LCD的硬件信息等等。

1):s3cfb_set_platdata函数分析:

 1 void __init s3cfb_set_platdata(struct s3c_platform_fb *pd)
 2 {
 3     struct s3c_platform_fb *npd;  // 定义一个 struct s3c_platform_fb 类型的指针 
 4     int i;
 5 
 6     if (!pd)                      // 如果没有传入 s3c_platform_fb 结构体变量指针,则使用默认的
 7         pd = &default_fb_data;
 8 
 9     npd = kmemdup(pd, sizeof(struct s3c_platform_fb), GFP_KERNEL);
10     if (!npd)
11         printk(KERN_ERR "%s: no memory for platform data
", __func__);
12     else {
13         for (i = 0; i < npd->nr_wins; i++)
14             npd->nr_buffers[i] = 1;
15 
16         npd->nr_buffers[npd->default_win] = CONFIG_FB_S3C_NR_BUFFERS; // 再进一步对数据结构进行填充
17 
18         s3cfb_get_clk_name(npd->clk_name);
19         npd->clk_on = s3cfb_clk_on;
20         npd->clk_off = s3cfb_clk_off;
21 
22         /* starting physical address of memory region */
23         npd->pmem_start = s5p_get_media_memory_bank(S5P_MDEV_FIMD, 1);
24         /* size of memory region */
25         npd->pmem_size = s5p_get_media_memsize_bank(S5P_MDEV_FIMD, 1);
26 
27         s3c_device_fb.dev.platform_data = npd;     // 把传进来的 s3c_platform_fb 结构体变量挂载到  s3c_device_fb变量中
28     }
29 }

总结:  由上可知s3cfb_set_platdata函数设置平台设备的私有数据,就是定义一个struct s3c_platform_fb类型的指针,然后给他申请分配内存然后进行一系列的填充,

最后将这个结构体挂接到平台设备的私有数据中去。

原文地址:https://www.cnblogs.com/deng-tao/p/6078072.html