100ask 韦东山 驱动大全 同步互斥 LCD驱动 学习笔记
同步与互斥 atomic，适用于单个变量的操作，不可分割，无锁操作。 spinlock, 忙等，一直占用 cpu, 只能由于非常短的临界区，比较适合多核系统。 semaphore, 用于多个线程访问临界资源，阻塞睡眠。 mutex, 只有一个线程访问临界资源，阻塞睡眠，比 spinlock 更加适合单核系统。 LCD驱动 硬件原理 像素表示: 24位的 RGB888, 16位的 RGB565 和 RGB555。 MIPI-DBI, 8080接口：GRAM一般用 SRAM 和 lcd 控制器 与屏幕，封装在一起，是 LCM. MIPI-DPI, TFT RGB 接口: GRAM 一般用可能是外接内存芯片比如 DDR 和 SDRAM，LCD 控制器在 ARM 芯片内部，屏幕就是单纯的屏幕。 Framebuffer驱动程序框架 module_init(fbmem_init) -> fbmem_init -> ret = register_chrdev(FB_MAJOR, "fb", &fb_fops); -> static const struct file_operations fb_fops fb_open static int fb_open(struct inode *inode, struct file *file) __acquires(&info->lock) __releases(&info->lock) __acquires(&info->lock) __releases(&info->lock) 用于注解本函数在使用中会获取和释放锁，同样的还有 __must_hold(&lock) 这样的表示调用函数时必须已经持有锁。 这种注释可以用静态代码分析工具（如 Sparse）来检查。 获取 info，获取时，使用 atomic_inc(&fb_info->count) 增加资源使用量，释放时，使用 atomic_dec_and_test(&fb_info->count) 减少资源使用量，当资源使用量为 0 的时候，返回 true，就可以 destory 资源。 int fbidx = iminor(inode); struct fb_info *info; info = get_fb_info(fbidx); // static struct fb_info *get_fb_info(unsigned int idx) mutex_lock(&registration_lock); fb_info = registered_fb[idx]; if (fb_info) atomic_inc(&fb_info->count); mutex_unlock(&registration_lock); // static void put_fb_info(struct fb_info *fb_info) { if (!atomic_dec_and_test(&fb_info->count)) return; if (fb_info->fbops->fb_destroy) fb_info->fbops->fb_destroy(fb_info); } open if (info->fbops->fb_open) { res = info->fbops->fb_open(info,1); if (res) module_put(info->fbops->owner); } fb_write static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) 获取 info struct fb_info *info = file_fb_info(file); write, 先调用 fb_write，然后使用 copy_from_user 从用户空间的 buf 中复制内容到 buffer 中，再使用 fb_memcpy_tofb 把 buffer 中的数据复制到 info 指向的显存地址中去。 if (info->fbops->fb_write) return info->fbops->fb_write(info, buf, count, ppos); // ... buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count, GFP_KERNEL); // ... dst = (u8 __iomem *) (info->screen_base + p); // ... while (count) { c = (count > PAGE_SIZE) ? PAGE_SIZE : count; src = buffer; if (copy_from_user(src, buf, c)) { err = -EFAULT; break; } fb_memcpy_tofb(dst, src, c); // ... } fb_compat_ioctl static long fb_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg) fb_compat_ioctl -> ret = do_fb_ioctl(info, cmd, arg); -> var = info->var; ret = copy_to_user(argp, &var, sizeof(var)) ? -EFAULT : 0; 总结 可以看出来，open, ioctl 之类的，都需要用到 info, 而 info->var 是 struct fb_var_screeninfo 其中包含了分辨率，rgb 等多种信息。 write 中，用到 info->fbops->fb_write，对应的是 struct fb_info -> struct fb_ops *fbops -> fb_write. 其他 open 之类的也类似。 驱动框架 前面 open, write 等当中，使用的都是 fb_info，主要调用的就是 struct fb_var_screeninfo var; 和 struct fb_ops *fbops;, 一个是屏幕信息，一个是操作函数。 查找 fb_info 的来源，fb_write -> file_fb_info -> struct fb_info *info = registered_fb[fbidx]; 说明来源于 registered_fb 这个数组。 查找写入 registered_fb 的地方 找到 struct fb_info *registered_fb[FB_MAX] __read_mostly; __read_mostly; 用于经常读取很少写入，会被加载到 cpu 缓存中； registered_fb 在 static int do_register_framebuffer(struct fb_info *fb_info) 中被写入，但是这个是 static 函数，还要继续找调用这个函数的地方。 找到 do_register_framebuffer 被 int register_framebuffer(struct fb_info *fb_info) 调用，说明其他文件中，需要调用的是 register_framebuffer 这个函数，来存入 fb_info 。 搜索其他使用 register_framebuffer 的地方，发现确实很多驱动都是调用这个函数的。 s3c2410fb.c s3c2410fb_init -> int ret = platform_driver_register(&s3c2410fb_driver); struct platform_driver s3c2410fb_driver -> .probe = s3c2410fb_probe, -> return s3c24xxfb_probe(pdev, DRV_S3C2410); static int s3c24xxfb_probe(struct platform_device *pdev, enum s3c_drv_type drv_type) // alloc 一个 fb_info 和私有结构体 s3c2410fb_info 的空间，s3c2410fb_info 在 fb_info 后面，fb_info->par = s3c2410fb_info fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev); // ... // 指针指向 s3c2410fb_info，然后配置 s3c2410fb_info info = fbinfo->par; info->dev = &pdev->dev; info->drv_type = drv_type; // ... // 配置 fbinfo 的 fix, var 和 fbops fbinfo->fix.type = FB_TYPE_PACKED_PIXELS; fbinfo->fix.type_aux = 0; fbinfo->fix.xpanstep = 0; fbinfo->fix.ypanstep = 0; fbinfo->fix.ywrapstep = 0; fbinfo->fix.accel = FB_ACCEL_NONE; fbinfo->var.nonstd = 0; fbinfo->var.activate = FB_ACTIVATE_NOW; fbinfo->var.accel_flags = 0; fbinfo->var.vmode = FB_VMODE_NONINTERLACED; fbinfo->fbops = &s3c2410fb_ops; // ... // 部分硬件配置 s3c2410fb_init_registers(fbinfo); // ... // 注册 fbinfo ret = register_framebuffer(fbinfo); 编写框架 init, exit init 中，alloc fb_info, 设置 fb_info, 注册 fb_info. alloc fb_info 可以根据自己的需要，增加需要的私有结构体 size 设置 fb_info，也就是 fix, var, ops var: 分辨率相关的 xres, yres, 像素大小的 bits_per_pixel, 像素在字节中占哪些位的 red green blue. fix: 和分辨率相关的显存大小 smem_len, 显存开始位置 smem_start, FBTYPE type, FBVISUAL visual . 在配置 smem_start 的时候，需要使用 dma_alloc_wc，这个函数返回的是虚拟地址需要放到 fbinfo->screen_base, 通过参数返回的是物理地址，配置给 smem_start . ops: 基本的 read, write 不需要自己实现，需要实现的是： 矩形 fb_fillrect, 区域复制 fb_copyarea, 图片显示 fb_imageblit 注册 fb_info, 使用 register_framebuffer. 基于 QEMU 的 lcd 驱动 按照 qemu 中预设的寄存器，用 ioremap 配置一下即可。 QEMU 上机 lcd 运行 ~/ubuntu-18.04_imx6ul_qemu_system/qemu-imx6ull-gui.sh qemu 的账号是 root 运行 fb-test 启动 lcd 显示测试程序。 配置 qemu 内核编译环境 可以参考官方页面 https://ldd.100ask.net/zh/03_LCD/05.html 下载源码 book@100ask:~$ mkdir -p 100ask_imx6ull-qemu && cd 100ask_imx6ull-qemu book@100ask:~/100ask_imx6ull-qemu$ ../repo/repo init -u https://e.coding.net/weidongshan/manifests.git -b linux-sdk -m imx6ull/100ask-imx6ull_qemu_release_v1.0.xml --no-repo-verify book@100ask:~/100ask_imx6ull-qemu$ ../repo/repo sync -j4 设置工具链 export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- export PATH=$PATH:/home/book/100ask_imx6ull-qemu/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin 配置、编译内核 book@100ask:~/100ask_imx6ull-qemu$ cd linux-4.9.88 book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make mrproper book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make 100ask_imx6ull_qemu_defconfig book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make zImage 在QEMU中使用新的zImage book@100ask:~$ cd ~/ubuntu-18.04_imx6ul_qemu_system/ book@100ask:~/ubuntu-18.04_imx6ul_qemu_system$ cp ~/100ask_imx6ull-qemu/linux-4.9.88/arch/arm/boot/zImage imx6ull-system-image/ 替换LCD驱动程序 只需要把新的驱动源码放到 linux-4.9.88/drivers/video/fbdev 里面， 然后修改 linux-4.9.88/drivers/video/fbdev/Makefile ，注释掉原有的 #obj-y += 100ask_qemu_fb.o, 再增加新的驱动即可。 最后使用 make zImage 编译内核。 启动 qemu 之后，可以通过 uname -a 来查看内核编译的时间，确认是新的内核。 调试 解决基本编译错误 根据调试情况，添加必须的，ops->fb_setcolreg， 并且对应的 s3c2410fb_setcolreg 中需要用到 FB_VISUAL_TRUECOLOR 中 u32 *pal = info->pseudo_palette;， 搜索 pseudo_palette 发现在 s3c24xxfb_probe 中，fbinfo->pseudo_palette = &info->pseudo_pal;, 这个 pseudo_pal 是私有结构体中的一个成员变量数组 所以，需要新增一个数组变量，并配置给 fbinfo. 根据调试情况，配置 fbinfo 中 var 的，虚拟分辨率xres_virtual, yres_virtual, xoffset, yoffset 根据调试情况，配置 fbinfo 中 fix 的，id, line_length. 结合 app 分析 lcd app 中，open 调用，fbmem 中的 open file->private_data = info;, 把 fbinfo 放入 file 中，但是因为 底层并没有配置 info->fbops->fb_open, 所以不会调用底层 open. app 中，mmap 调用, fbmem 中的 fb_mmap , 会先检查底层有没有配置 info->fbops->fb_mmap, 因为没有配置，所以使用 vm_iomap_memory 把 info->fix.smem_start info->fix.smem_len 映射到用户控制中。 TFT RGB 接口 IMX6ULL 的 dotclk 接口，使用 hsync 从上一行的末尾到下一行的开头，使用 vsync 从最后一行的末尾到第一行的开头。 eLCDIF 寄存器 主要用到的寄存器: LCDIF_CTRL: SFTRST: 复位 BYPASS_COUNT, DOTCLK_MODE: dotclk 模式 LCD_DATABUS_WIDTH: 总线宽度 WORD_LENGTH: 显存中每个像素位数; DATA_FORMAT_16_BIT: 555 还是 565 MASTER：bus master LCDIF_CTRL1: BYTE_PACKING_FORMAT 32位中哪些字节有效 LCDIF_TRANSFER_COUNT: V_COUNT 行数， H_COUNT 每行像素 LCDIF_VDCTRL0: VSYNC_OEB: 不同模式 VSYNC信号设置 ENABLE_PRESENT： 硬件产生使能信号 VSYNC_POL, HSYNC_POL, DOTCLK_POL, ENABLE_POL： VSYNC, HSYNC, DOTCLK, ENABLE 脉冲信号极性 VSYNC_PERIOD_UNIT, VSYNC_PULSE_WIDTH_UNIT: VSYNC_PULSE_WIDTH: VSYNC 脉冲的宽度 LCDIF_VDCTRL1, LCDIF_VDCTRL2 : VSYNC, HSYNC 信号宽度和间隔 LCDIF_VDCTRL3: HORIZONTAL_WAIT_CNT, VERTICAL_WAIT_CNT 水平和垂直上的等待个数 LCDIF_VDCTRL4: SYNC_SIGNALS_ON dotclk 设置位1；DOTCLK_H_VALID_DATA_CNT 水平方向上的像素个数 LCDIF_CUR_BUF, LCDIF_NEXT_BUF: 当前帧和下一帧在显存中的地址 分析内核自带驱动 官方放出的源码，编译之后，ls drivers/video/fbdev/*.o 查看编译后的文件，也可以使用 make menuconfig 查看 fb 开启的条目，可以查看对应源码文件中的内容，imx6ull 对应的是 mxsfb.c 相关宏 module_platform_driver module_platform_driver(mxsfb_driver); module_platform_driver 是 Linux 内核中用于简化平台设备驱动（Platform Driver）注册和注销的一个宏，自动生成入口和出口函数，自动注册和卸载。 MODULE_DEVICE_TABLE MODULE_DEVICE_TABLE 是 Linux 内核中用于 导出设备表 的宏，它使得内核模块能够与设备匹配，并在加载时自动绑定到相应的设备。根据设备和驱动的匹配方式，MODULE_DEVICE_TABLE 可以用于不同的设备表类型，例如 平台设备 和 设备树。 MODULE_DEVICE_TABLE(platform, mxsfb_devtype); static const struct platform_device_id mxsfb_devtype[] 内核能够在加载模块时将其与平台设备匹配, 用于传统的平台设备匹配机制（非设备树）。 MODULE_DEVICE_TABLE(of, mxsfb_dt_ids); static const struct of_device_id mxsfb_dt_ids[] 内核能够在加载模块时将其与设备树节点匹配, 用于设备树（Device Tree）匹配机制。 mxsfb_dt_ids->compatible 和 设备树中的 compatible 进行匹配 设备树 struct platform_driver mxsfb_driver 中 mxsfb_driver->driver->of_match_table 对应 mxsfb_dt_ids, 查看 const struct of_device_id mxsfb_dt_ids[] 中的 .compatible 这个是和设备树匹配的。 在内核中 arch/arm/boot/dts 可以 grep -nr --include="*.dts" --include="*.dtsi" "fsl,imx28-lcdif" 搜索设备树匹配到的内容。 dtsi 中只指定了最基本的一些寄存器信息，具体的单板相关的还要找 dts 文件。 可以通过 grep -nr --include="*.dts" --include="*.dtsi" "include \"imx6ull.dtsi\"" 来查找有哪些文件引用了 dtsi 打开单板 dts 文件后，搜索 &lcdif 来查看具体的 lcd 相关的单板配置。 这个 lcdif 来源于 dtsi 中的 tag. 驱动代码 struct platform_driver mxsfb_driver 中 mxsfb_driver->.probe 对应 mxsfb_probe res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 从设备树中，获取资源。IORESOURCE_MEM 指定内存资源。 host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL); 申请 mxsfb 自用的结构体 包含 fbinfo 和 硬件等相关信息。 devm_kzalloc 用于设备资源管理的内存分配函数。是 kzalloc 的增强版，专门为设备驱动设计，能够自动管理分配的内存，初始化为 0，当设备卸载或驱动退出时，内核会自动释放这块内存，避免内存泄漏问题。 fb_info = framebuffer_alloc(0, &pdev->dev); 申请 fbinfo 结构体。 ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0, dev_name(&pdev->dev), host);, 参数 host 是 mxsfb 自用结构体，devm_request_irq 可以让后续的中断处理函数，使用 host 这个结构体。 host->base = devm_ioremap_resource(&pdev->dev, res); devm_ioremap_resource 用于设备资源管理的内存映射函数。作用是将设备的 I/O 内存资源映射到内核的虚拟地址空间，并自动管理映射的生命周期，避免资源泄漏。 host->devdata = &mxsfb_devdata[pdev->id_entry->driver_data];, mxsfb_devdata 中的 MXSFB_V3 用了 enum 的默认转为 0 之类的数值。 enum mxsfb_devtype { MXSFB_V3, MXSFB_V4, MXSFB_V5, }; static const struct mxsfb_devdata mxsfb_devdata[] = { [MXSFB_V3] = { .transfer_count = LCDC_V3_TRANSFER_COUNT, .cur_buf = LCDC_V3_CUR_BUF, .next_buf = LCDC_V3_NEXT_BUF, .debug0 = LCDC_V3_DEBUG0, .hs_wdth_mask = 0xff, .hs_wdth_shift = 24, .ipversion = 3, .flags = MXSFB_FLAG_NULL, }, [MXSFB_V4] = { .transfer_count = LCDC_V4_TRANSFER_COUNT, .cur_buf = LCDC_V4_CUR_BUF, .next_buf = LCDC_V4_NEXT_BUF, .debug0 = LCDC_V4_DEBUG0, .hs_wdth_mask = 0x3fff, .hs_wdth_shift = 18, .ipversion = 4, .flags = MXSFB_FLAG_BUSFREQ, }, [MXSFB_V5] = { .transfer_count = LCDC_V4_TRANSFER_COUNT, .cur_buf = LCDC_V4_CUR_BUF, .next_buf = LCDC_V4_NEXT_BUF, .debug0 = LCDC_V4_DEBUG0, .hs_wdth_mask = 0x3fff, .hs_wdth_shift = 18, .ipversion = 4, .flags = MXSFB_FLAG_PMQOS, }, }; static const struct platform_device_id mxsfb_devtype[] = { { .name = "imx23-fb", .driver_data = MXSFB_V3, }, { .name = "imx28-fb", .driver_data = MXSFB_V4, }, { .name = "imx7ulp-fb", .driver_data = MXSFB_V5, }, { /* sentinel */ } }; host->clk_pix = devm_clk_get(&host->pdev->dev, "pix"); devm_clk_get 用于 设备资源管理 的时钟获取函数。它的作用是从设备中获取时钟信号（clock），并自动管理时钟资源的生命周期，避免资源泄漏。 可以从设备树中，获取对应 "pix" 的数值。 host->reg_lcd = devm_regulator_get(&pdev->dev, “lcd”); 用于 设备资源管理 的电压调节器（Regulator）获取函数。它的作用是从设备中获取电压调节器，并自动管理调节器资源的生命周期，避免资源泄漏。 可以从设备树中，获取对应 "lcd" 的数值。 pm_runtime_enable(&host->pdev->dev); 用于 运行时电源管理 的函数。它的作用是启用设备的运行时电源管理功能，使得设备可以根据需要自动进入低功耗状态（如挂起）或恢复全功率状态（如唤醒）。 probe 函数中启用运行时电源管理。pm_runtime_enable(&pdev->dev); 设置设备为活动状态（唤醒设备）pm_runtime_get_sync(&pdev->dev); 设置设备为空闲状态（允许挂起） pm_runtime_put_sync(&pdev->dev); remove 函数中禁用运行时电源管理。 pm_runtime_disable(&pdev->dev); 挂起设备（进入低功耗状态）pm_runtime_suspend, 恢复设备（退出低功耗状态） pm_runtime_resume pm_runtime_set_active 设置设备为活动状态（忽略引用计数）, pm_runtime_set_suspended 设置设备为挂起状态（忽略引用计数）。 ret = mxsfb_init_fbinfo(host); 配置 fbinfo ret = mxsfb_init_fbinfo_dt(host); 配置 fbinfo 的数据来自于设备树 host->id = of_alias_get_id(np, "lcdif"); of_alias_get_id 用于 从设备树别名（alias）中获取设备 ID 的函数。它的作用是根据设备树中的别名（aliases 节点）获取设备的唯一 ID，通常用于区分多个相同类型的设备。 display_np = of_parse_phandle(np, "display", 0); of_parse_phandle 用于 从设备树中解析句柄（phandle） 的函数。它的作用是从设备树中获取一个指向另一个设备节点的引用（即句柄），并返回该节点的 device_node 结构体指针。 timings = of_get_display_timings(display_np); of_get_display_timings 用于 从设备树中获取显示时序（display timings） 的函数。它的作用是从设备树中解析显示设备的时序信息 "display-timings"，并返回一个 struct display_timings 结构体指针，用于配置显示设备。 fb_videomode_to_var(var, &modelist->mode); 用 fbinfo 中的 video 数据来配置 varscreen 数据 if (mxsfb_map_videomem(fb_info) < 0) 分配显存。 ret = register_framebuffer(fb_info); 注册 fbinfo 硬件相关 lcdif 下面有 pinctrl-0 = <&pinctrl_lcdif_dat 这样的，可以搜索 pinctrl_lcdif_dat 就可以获得更加详细的 gpio 配置。 这些配置一般使用厂商的工具生成即可。 &lcdif 下面有 display-timings, 其中的 clock-frequency 这个需要和 lcd 规格书里面的 dotclock 匹配。具体解析内核代码在 drivers/video/of_display_timing.c 中。 static int of_parse_display_timing(const struct device_node *np, struct display_timing *dt) 会匹配设备树中的字符串，然后把相关数值给到 结构体成员中。 drivers/video/videomode.c 中，void videomode_from_timing(const struct display_timing *dt, struct videomode *vm) 会把 display_timing 中的 dt->pixelclock.typ 转化为 videomode 中的 vm->pixelclock drivers/video/fbdev/mxc/ldb.c 中， static int ldb_setup(struct mxc_dispdrv_handle *mddh, struct fb_info *fbi) 会 serial_clk = ldb->spl_mode ? chan.vm.pixelclock * 7 / 2 : chan.vm.pixelclock * 7; 先计算，然后再 clk_set_rate(ldb_di_sel_parent, serial_clk); 写入。 还可以从寄存器反着推算。 probe 函数中，host->base = devm_ioremap_resource(&pdev->dev, res);, 说明寄存器基地址是 host->base; 搜索 host->base 可以看到 ctrl1 = readl(host->base + LCDC_CTRL1); 再搜索 LCDC_CTRL 就可以看到寄存器宏定义的地方，然后再根据寄存器偏移地址，获取寄存器宏名称 #define LCDC_V4_TRANSFER_COUNT 0x30 搜索 LCDC_V4_TRANSFER_COUNT 得到 .transfer_count = LCDC_V4_TRANSFER_COUNT,, 搜索 transfer_count 得到 writel(TRANSFER_COUNT_SET_VCOUNT(fb_info->var.yres) |TRANSFER_COUNT_SET_HCOUNT(fb_info->var.xres), host->base + host->devdata->transfer_count); 在 static int mxsfb_set_par(struct fb_info *fb_info) 中。 mxsfb_set_par 在 static struct fb_ops mxsfb_ops中，由系统调用. 整体来看，probe -> mxsfb_init_fbinfo -> mxsfb_init_fbinfo_dt 这个调用用于获取设备树数据并把 display_timing 先转为 videomode， 然后再转为 fb_videomode, 再加到 modelist 中；probe -> mxsfb_init_fbinfo -> fb_videomode_to_var 把 modelist中第一个转化为 fbinfo 的 varscreeninfo; 后续等系统调用 mxsfb_ops 中的 mxsfb_set_par 就可以写入寄存器了。 lcd 驱动框架 可以参考 drivers/video/fbdev/simplefb.c 按照这个框架就可以了。 引脚配置 可以使用 nxp 官方的 pin tools 工具，先加载预设配置，然后根据自己需要，选择引脚状态，然后就可以生成对应的设备树内容。 根据自己的需要，把里面的内容合并到源码设备树中。 具体写法，可以参考设备树已有的内容。 // root framebuffer-mylcd { compatible = "100ask,lcd_drv"; pinctrl-names = "default"; pinctrl-0 = <&mylcd_pinctrl>; backlight-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>; }; // &iomuxc / imx6ul-evk mylcd_pinctrl: mylcd_pingrp { /*!< Function assigned for the core: Cortex-A7[ca7] */ fsl,pins = < MX6UL_PAD_GPIO1_IO08__GPIO1_IO08 0x000010B0 MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x000010B0 MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x000010B0 MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x000010B0 MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x000010B0 MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x000010B0 MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x000010B0 MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x000010B0 MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x000010B0 MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x000010B0 MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x000010B0 MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x000010B0 MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x000010B0 MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x000010B0 MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x000010B0 MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x000010B0 MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x000010B0 MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x000010B0 MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x000010B0 MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x000010B0 MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x000010B0 MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x000010B0 MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x000010B0 MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x000010B0 MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x000010B0 MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x000010B0 MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x000010B0 MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x000010B0 MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x000010B0 >; }; probe 函数中关于背光: /* get gpio from device tree */ bl_gpio = gpiod_get(&pdev->dev, "backlight", 0); /* config bl_gpio as output */ gpiod_direction_output(bl_gpio, 1); /* set val: gpiod_set_value(bl_gpio, status); */ 时钟配置 在 imx6ull.dtsi 中，搜索 lcdif，就可以看到已经预先设置好了时钟，IMX6UL_CLK_DUMMY 这一项是用来占位的，为了兼容驱动程序而已。我们自己重新编写驱动程序和设备树，就可以省略这一条。 framebuffer-mylcd { compatible = "100ask,lcd_drv"; pinctrl-names = "default"; pinctrl-0 = <&mylcd_pinctrl>; backlight-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>; clocks = <&clks IMX6UL_CLK_LCDIF_PIX>, <&clks IMX6UL_CLK_LCDIF_APB>; clock-names = "pix", "axi"; }; 另外 axi 时钟因为是总线时钟，一般已经设置好了，不需要手动设置。 /* get clk from device tree */ clk_pix = devm_clk_get(&pdev->dev, "pix"); clk_axi = devm_clk_get(&pdev->dev, "axi"); /* set clk rate */ clk_set_rate(clk_pix, 50000000); /* enable clk */ clk_prepare_enable(clk_pix); clk_prepare_enable(clk_axi); lcd 控制器获得参数 设备树 设备树中，display = <&display0>;, native-mode = <&timing0>; 用于指明具体使用的配置。后续的配置分为两部分，一部分是时序，另外一部分是极性。 时序相关的直接参考规格书里面的时间表格即可。 极性需要对照时序图，确认有效边沿的情况，上升沿是 0，下降沿是 1. // framebuffer-mylcd display = <&displayA>; displayA: display { bits-per-pixel = <24>; bus-width = <24>; display-timings { native-mode = <&timingA>; timingA: timing0_1024x600 { clock-frequency = <50000000>; hactive = <1024>; vactive = <600>; hfront-porch = <160>; hback-porch = <140>; hsync-len = <20>; vback-porch = <20>; vfront-porch = <12>; vsync-len = <3>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; }; 代码 struct device_node *display_np; int ret; int width; int bits_per_pixel; struct display_timings *timings = NULL; display_np = of_parse_phandle(pdev->dev.of_node, "display", 0); /* get common info */ ret = of_property_read_u32(display_np, "bus-width", &width); ret = of_property_read_u32(display_np, "bits-per-pixel", &bits_per_pixel); /* get timming */ timings = of_get_display_timings(display_np); pdev->dev.of_node 这个指向了设备树中对应的节点。static const struct of_device_id mxsfb_dt_ids[] 中 .compatible = "fsl,imx28-lcdif" 和 imx6ull.dtsi 中的 lcdif 相匹配。 在 100ask_imx6ull-14x14.dts 中，&lcdif对 lcdif 进行了更加详细的配置。 所以，pdev->dev.of_node 实际对应的就是 &lcdif display_np = of_parse_phandle(pdev->dev.of_node, "display", 0); 获得了 display[0] 的 handle timings = of_get_display_timings(display_np); 对 time 进行获取 timings_np = of_get_child_by_name(np, "display-timings"); 获得 display-timings 节点 entry = of_parse_phandle(timings_np, "native-mode", 0); 获得 native-mode[0] 也就是 timing0 的 handle for_each_child_of_node(timings_np, entry) r = of_parse_display_timing(entry, dt); 对 timing0 进行解析。 ret |= parse_timing_property(np, "hback-porch", &dt->hback_porch); 这样的时间保存在 dt 中。 极性保存在 dt->flags 中。 if (!of_property_read_u32(np, "vsync-active", &val)) dt->flags |= val ? DISPLAY_FLAGS_VSYNC_HIGH : DISPLAY_FLAGS_VSYNC_LOW; lcd 寄存器配置 根据芯片手册，并参考官方提供的裸机代码，对寄存器配置即可。 根据芯片手册，定义一个寄存器结构体 lcdif，方便后续操作。并把相关寄存器操作放到一个函数里面。 结构体 lcdif 地址应该通过设备树来获取, 设备树中对应的节点，应该有类似于 reg 参数，驱动中才能获取这个 mem 参数。 framebuffer-mylcd { compatible = "100ask,lcd_drv"; reg = <0x021c8000 0x4000>; ... res = platform_get_resource(pdev, IORESOURCE_MEM, 0); lcdif = devm_ioremap_resource(&pdev->dev, res); 上机实验 替换驱动和设备树: makefile 中需要注释旧的，增加新的 设备树中，原来的 &lcdif 中的 status 需要改为 disabled 内核源码 make mrproper make 100ask_imx6ull_defconfig make zImage -j4 这样编译内核与驱动; make dtbs 编译设备树。 内核在 arch/arm/boot/zImage, 设备树arch/arm/boot/dts/100ask_imx6ull-14x14_ptz.dtb 重新烧录系统 如果系统有问题，需要重新烧录： 开发板设置为 usb 启动，然后用 usb 连接 使用专门的烧录工具进行烧录，烧录整个系统。 具体可以参考开发板使用手册上面的，烧写系统的章节 烧录完成之后，断电，并设置为 emmc 启动，然后重新上电。 调试 查看 /dev/fb0 有没有这个设备 查看 /sys/firmware/devicetree/base/framebuffer-mylcd, cat compatible 看看是否和设备树文件以及驱动 struct of_device_id mxsfb_dt_ids[] 中的 compatible 一致。 查看 /sys/bus/platform/drivers 下面有没有和 struct platform_driver mxsfb_driver 中的 .driver.name 同名的平台驱动文件夹。 如果既有设备树，也有平台驱动，但是没有 /dev/fb0 这个设备。 唯一的可能就是 probe 没有工作。需要检查开机时候的启动信息。 如果出现引脚被占用，那么需要把引脚在其他地方的 status 改为 disabled. 单 buffer 的缺点与改进方法 缺点 因为 app 和 lcd 控制器，使用同一个 buffer，就可能产生两种情况: app 慢，buffer 中才更新了一般，lcd 已经刷新完成 app 快，lcd 才显示一部分，buffer 中的数据已经是下一帧的数据，导致 lcd 下半部分是新的数据。 都会导致画面撕裂，闪烁之类的情况。 改进 就是使用多 buffer 来改进。 驱动 alloc 多 buffer 驱动描述 多 buffer app 获取 多 buffer app 写入数据 app 启动切换 buffer 驱动执行切换 buffer 代码 alloc 多 buffer mxsfb.c 中 fb_info->fix.smem_len = SZ_32M; 其他开发板使用的 framesize = width * height * 2 * 2; fb->fb.fix.smem_len = framesize; 双倍 buffer. 然后使用 dma_alloc_writecombine 这样的函数来申请内存空间。 描述 多 buffer struct fb_var_screeninfo { __u32 xres; /* visible resolution */ __u32 yres; __u32 xres_virtual; /* virtual resolution */ __u32 yres_virtual; __u32 xoffset; /* offset from virtual to visible */ __u32 yoffset; /* resolution */ 假设分辨率是 1024 x 600, 申请的是 3倍 buffer. 申请的多个 buffer 本身是在一起的连续内存。 xres_virtual 和 yres_virtual 指的是多个 buffer 在一起的总的尺寸。那么虚拟分辨率就是 1024 x 1800 xres yres 可视分辨率还是 1024 x 600 可视分辨率和虚拟分辨率的关系是，在虚拟分辨率上，通过 xoffset 和 yoffset 的偏移，获得的就是 可视分辨率。 如果现在使用第二个 fb，那么 xoffset 和 yoffset 就是 (0, 600) 开始时，只能用单 buffer，也就是虚拟与可视一致。 app 获取 多 buffer ioctl(fbfd, FBIOGET_FSCREENINFO, &m_finfo), ioctl(fbfd, FBIOGET_VSCREENINFO, &m_vinfo) 获取 fix 和 var 数据。 m_nPhysicalMemNum = m_finfo.smem_len / m_nMemSize; 通过 fix 和 var 中的显存大小和可视分辨率，可以计算出，申请的显存可以容纳多少个 fb; 然后对比 var 中的虚拟分辨率，确认有没有问题，如果有问题，需要使用 ioctl(fbfd, FBIOPUT_VSCREENINFO, &m_vinfo) 来重新设置。 app 写入数据 m_pMem = (char *)mmap(0, m_finfo.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0); 映射显存。 m_pVirtualMem[i] = m_pMem + i * m_nMemSize; 定位相应的 fb，然后就可以写入了。 app 启动切换 buffer ioctl(fbfd, FBIOPAN_DISPLAY, &m_vinfo) 在修改 yoffset 之后，执行这个，来让驱动执行切换。 驱动执行切换 buffer .fb_pan_display = mxsfb_pan_display, 因为 ops 中设置了这个，所以系统会调用 static int mxsfb_pan_display(struct fb_var_screeninfo *var, struct fb_info *fb_info) writel(fb_info->fix.smem_start + offset, host->base + host->devdata->next_buf); 新的 fb 地址写入到下一帧寄存器 writel(CTRL1_CUR_FRAME_DONE_IRQ_EN, host->base + LCDC_CTRL1 + REG_SET); 设置当前帧完成中断使能 ret = wait_for_completion_timeout(&host->flip_complete, HZ / 2); 等待中断完成。完成之后，会有个返回给 app. 其他 新的驱动中，一般直接等待中断完成，然后 app 直接 if(ioctl(fbfd, FBIOPAN_DISPLAY, &m_vinfo) == -1) 检查返回值就行。 老的驱动中，没有这个等待中断完成，需要 app 主动等待 vsync 信号: m_vinfo.yoffset = nMemMap * m_nPhysicalHeight; ret = ioctl(m_fd, FBIOPAN_DISPLAY, &m_vinfo); if(ret == 0) { m_nDispMemNo = nMemMap; ioctl(m_fd, FBIO_WAITFORVSYNC, &ret); } 编写 多 buffer 应用程序 按照上节的步骤，编写应用程序即可。 调试的时候，需要先移除开发板自身的 gui 程序: /etc/init.d/S99myirhmi2 移除这个自启动文件 然后执行应用程序即可。 如果闪屏比较严重，可以在 app 中使用 nanosleep 来减慢速度，降低显存改变的频率，效果会好很多。
在嵌入式平台 rk3399 上面进行本地编译，会碰到 x86 上没有的问题。 [...]
c89，不能在 for 循环定义变量，很麻烦，所以决定用 c99。在 Makefile 里面的写法如下： ccflags-y := -std=gnu99 -Wno-declaration-after-statement 参考：https://blog.csdn.net/yangkunhenry/article/details/94703005
当在 source insight 中想要搜索特定的符号，比如说 "struct file {"，需要在搜索的时候，把 search method 中的选择，改为 simple string，这样虽然慢一点，但是确实能搜索出来。 参考： https://zhidao.baidu.com/question/1859160069462990747.html
100ask 韦东山 第五篇 嵌入式 Linux 驱动开发基础知识 视频零散知识点
https://www.bilibili.com/video/BV14f4y1Q7ti 2 hello 驱动 可以参考 drivers\char\misc.c 这个是非常经典的字符设备驱动程序。 printk 打印信息，可以通过 dmesg 来查看。 9 总线设备驱动模型 platform_device 用于存放设备的资源相关. struct resource 在 include\linux\ioport.h 中, struct resource 给出明确的定义，需要按照这种形式去写。 struct resource { resource_size_t start; resource_size_t end; const char *name; unsigned long flags; unsigned long desc; struct resource *parent, *sibling, *child; }; 资源也可以放在 platform_device -> struct device dev -> void *platform_data 定义在 include\linux\device.h 中，这个 platform_data 那就是根据需要完全自己定义就行。 platform_driver 用于存放操作相关的函数。 bus 维持两个链表，一个是 device 链表，一个是 driver 链表，两个链表中有任何一个新增，就会扫描对方链表，查看是否有匹配，probe. drivers\base\platform.c 中的 struct bus_type platform_bus_type 定义了匹配用到 platform_match struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups, .match = platform_match, .uevent = platform_uevent, .pm = &platform_dev_pm_ops, }; 具体的匹配，可以看 drivers\base\platform.c 中的 static int platform_match(struct device *dev, struct device_driver *drv) 可以看到匹配的优先级。 /* When driver_override is set, only bind to the matching driver */ if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); /* Attempt an OF style match first */ if (of_driver_match_device(dev, drv)) return 1; /* Then try ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; /* Then try to match against the id table */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* fall-back to driver name match */ return (strcmp(pdev->name, drv->name) == 0); 11 设备树处理与使用 根节点 of_root 可以通过 device_node 中的 parent child 来遍历所有节点。 子节点能否转换为 platform_device, 取决于父节点的 comptible 是否是 "simple-bus","simple-mfd","isa","arm,amba-bus", 而且子节点也要有 comptible 当前设备树 /sys/firmware/devicetree/base, 搜索 ls -ld *100ask* cd 打开设备节点文件夹，里面有 comptible, name, pin，可以用 cat, hexdump 来读取数据 打开platform_device对应的文件夹 /sys/devices/platform 或者 /sys/devices/soc0 , 然后 ls -ld *100ask* 也可以找到对应的设备节点。文件夹下面的 of_node 指向设备树里面的节点。 加载驱动之后，自动 probe了，在platform_device对应的文件夹下，会多出来一个 driver 文件夹，指向了 /sys/devices/soc0/100ask_led@0/driver 这个对应的驱动文件夹。并且 uevent 这个文件里面，也多了 driver 这一行。 打开 驱动文件夹 /sys/devices/soc0/100ask_led@0/driver，可以看到下面 100ask_led@0, 100ask_led@1, 分别指向了设备文件夹下面的设备节点。module 指向了驱动模块文件夹 /sys/devices/soc0/100ask_led@0/driver/module。 16 2gpio子系统 #gpio-cells = <2>; 值得是，除了 gpioX 这个以外，还要两个数值。 gpios 或者 xxx-gpios 可以指定多个引脚。 led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */ <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */ <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */ 只有一个引脚的时候，使用 gpiod_get。多个引脚取一个的时候，使用 gpiod_get_index 16_4 上机 设备树中新增 pinctrl 相关 &iomuxc_snvs { …… myled_for_gpio_subsys: myled_for_gpio_subsys{ fsl,pins = < MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x000110A0 >; }; 设备树中新增节点 myled { compatible = "100ask,leddrv"; pinctrl-names = "default"; pinctrl-0 = <&myled_for_gpio_subsys>; led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; }; 检查这个引脚有没有其他地方使用，可以通过 MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 来查找，找到使用的设备节点后，在里面增加 status = "disabled"; 禁用。 18_2 中断处理的演进 中断下半部开始执行的时候，开了中断. 如果在下半部被中断了，那么第二次的中断上半部执行完后，后发现 preempt_count 不为0，就不执行下半部了。 这样就会回到第一次被中断的下半部，继续执行完成下半部的处理。 这就是上下多对一。 并且下半部会处理所有的软件中断。 18_3 中断系统数据结构 GIC, GPIO 这些都是芯片内部的，BSP 可以直接处理，比如说，发生了一个中断，那就先去看看 GIC 里面，是哪个模块发生了中断？ 如果是 GPIO 模块中断了，再去 GPIO 这边处理下。 实际的操作流程是，中断发生之后， GIC 的 irq_desc.handle_irq 内部分辨是哪个模块，然后自动调用 GPIO 的 irq_desc.handle_irq gpio 会挨个调用中断引脚的处理函数。 也就是通过 irq_desc.action 链表，挨个调用链表中的 irq_action.handler irq_action.handler 上半部结束后会调用 irq_action.thread_fn下半部 irq_action.thread_fn 涉及到的线程是 irq_action.thread irq_action 由 request_irq() 注册的。 request_irq() 用到的中断号并不是从设备树中直接拿来用的。需要 irq_domain.ops.xlate 先从设备树进行解析，然后 irq_domain.ops.map 对设备树中的硬件中断和 request_irq() 用到的中断号进行映射。 映射关系保存在 irq_domain.linear_revmap[] 中。 irq_chip 里面的 irq_enable() irq_disable() 会被自动调用。 实际上，GIC 中断之后，是需要先通过 irq_desc.irq_data.domain.linear_revmap[] 来获取到中断号对应的硬件中断号，然后才能去调用 GPIO 的 irq_desc.handle_irq。 18_4 代码中获得中断 设备树中，一定要看清楚 interrupt_parent 是谁，并且对应的 #interrupt-cells 是多少，不同层级可能这个数目是不一样多的。 如果本层没有指明 interrupt_parent ，那可能是继承了上一层的，去上一层查看。 19_9 mmap 基础 cat /proc/pid/maps 可以查看进程使用的内存情况。 一般不适用 cache register framebuffer dma 一般不适用于 写缓冲，可能会有多个字节合并为 word 的可能。 register 19_10 mmap 编程 mmap 函 数 MAP_SHARED 、 MAP_PRIVATE 参 数 。 MAP_PRIVATE 会导致，当对映射的 buf 进行写入，那么内核会自动复制一块内存，并让 buf 指向它，只有这个 buf 才有新的数据。就算是驱动程序也只是旧内存。非常适合需要临时修改文件内容或创建文件副本的场景。
100ask 韦东山 第五篇 嵌入式 Linux 驱动开发基础知识 学习笔记
第1章 Hello 驱动(不涉及硬件操作) 1.1 APP 打开的文件在内核中如何表示 使用 open 打开文件时，传入的 flags、mode 等参数， 读写文件时，文件的当前偏移地址，对文件的操作api，都在 include\linux\fs.h 的 struct file 中。 struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; /* * Protects f_ep_links, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; ... } 1.2 打开字符设备节点时，内核中也有对应的 struct file // include\linux\fs.h struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ... long (*compat_ioctl) (struct file *, unsigned int, unsigned long); ... int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); ... } 1.3 请猜猜怎么编写驱动程序 谁来注册驱动程序啊？得有一个入口函数：安装驱动程序时，就会去调用这个入口函数 有入口函数就应该有出口函数：卸载驱动程序时，出口函数调用 unregister_chrdev 把 file_operations 结构体告诉内核：register_chrdev 确定主设备号，也可以让内核分配 定义自己的 file_operations 结构体 实现对应的 drv_open/drv_read/drv_write 等函数，填入 file_operations 结构体 其他完善：提供设备信息，自动创建设备节点：class_create, device_create 1.4 编写代码 1.4.1 写驱动程序 入口和出口空程序体 #include <linux/module.h> // init static int __init hello_init(void) { } // exit static void __exit hello_exit(void) { } // module_init(hello_init); module_exit(hello_exit); // license MODULE_LICENSE("GPL"); init 里面调用 drivers\base\core.c 中的 struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) 。 其中 struct class, struct device 都是在 include/linux/device.h 里面定义的。 exit 里面调用相反的 void device_destroy(struct class *class, dev_t devt) #include <linux/device.h> // static int hello_major; static dev_t hello_devt; struct class * hello_class; struct device * hello_deivce; // init static int __init hello_init(void) { hello_devt = MKDEV(hello_major, 0); /** * device_create - creates a device and registers it with sysfs * @class: pointer to the struct class that this device should be registered to * @parent: pointer to the parent struct device of this new device, if any * @devt: the dev_t for the char device to be added * @drvdata: the data to be added to the device for callbacks * @fmt: string for the device's name * * This function can be used by char device classes. A struct device * will be created in sysfs, registered to the specified class. * * A "dev" file will be created, showing the dev_t for the device, if * the dev_t is not 0,0. * If a pointer to a parent struct device is passed in, the newly created * struct device will be a child of that device in sysfs. * The pointer to the struct device will be returned from the call. * Any further sysfs files that might be required can be created using this * pointer. * * Returns &struct device pointer on success, or ERR_PTR() on error. * * Note: the struct class passed to this function must have previously * been created with a call to class_create(). */ hello_deivce = device_create(hello_class, NULL, hello_devt, NULL, "hello") } // exit static void __exit hello_exit(void) { /** * device_destroy - removes a device that was created with device_create() * @class: pointer to the struct class that this device was registered with * @devt: the dev_t of the device that was previously registered * * This call unregisters and cleans up a device that was created with a * call to device_create(). */ device_destroy(hello_class, hello_devt); } 根据 device_create 注释中说明的，需要先用 class_create(owner, name) , 在 include\linux\device.h 里面的宏. 还有对应的 class_destroy(struct class *cls) 。 当然，都是调用 drivers\base\class.c 里面的函数 static int __init hello_init(void) { /** * class_create - create a struct class structure * @owner: pointer to the module that is to "own" this struct class * @name: pointer to a string for the name of this class. * @key: the lock_class_key for this class; used by mutex lock debugging * * This is used to create a struct class pointer that can then be used * in calls to device_create(). * * Returns &struct class pointer on success, or ERR_PTR() on error. * * Note, the pointer created here is to be destroyed when finished by * making a call to class_destroy(). */ hello_class = class_create(THIS_MODULE, "hello"); if (IS_ERR(hello_class)) { printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_class)); return PTR_ERR(hello_class); } hello_devt = MKDEV(hello_major, 0); hello_deivce = device_create(hello_class, NULL, hello_devt, NULL, "hello") if (IS_ERR(hello_deivce)) { printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_deivce)); class_destroy(hello_class); return PTR_ERR(hello_deivce); } } // static void __exit hello_exit(void) { device_destroy(hello_class, hello_devt); class_destroy(hello_class); } major 需要 include\linux\fs.h 中的 static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) 来生成。 #include <linux/fs.h> static struct file_operations * hello_fops; // static int __init hello_init(void) { /** * __register_chrdev() - create and register a cdev occupying a range of minors * @major: major device number or 0 for dynamic allocation * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: name of this range of devices * @fops: file operations associated with this devices * * If @major == 0 this functions will dynamically allocate a major and return * its number. * * If @major > 0 this function will attempt to reserve a device with the given * major number and will return zero on success. * * Returns a -ve errno on failure. * * The name of this device has nothing to do with the name of the device in * /dev. It only helps to keep track of the different owners of devices. If * your module name has only one type of devices it's ok to use e.g. the name * of the module here. */ hello_major = register_chrdev(0, "hello", hello_fops); if (hello_major < 0) { printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, hello_major); return hello_major; } hello_class = class_create(THIS_MODULE, "hello"); if (IS_ERR(hello_class)) { printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_class)); unregister_chrdev(hello_major, "hello"); return PTR_ERR(hello_class); } hello_devt = MKDEV(hello_major, 0); hello_deivce = device_create(hello_class, NULL, hello_devt, NULL, "hello") if (IS_ERR(hello_deivce)) { printk("%s %s line %d, error %d\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_deivce)); class_destroy(hello_class); unregister_chrdev(hello_major, "hello"); return PTR_ERR(hello_deivce); } } // static void __exit hello_exit(void) { device_destroy(hello_class, hello_devt); class_destroy(hello_class); unregister_chrdev(hello_major, "hello"); } 初始化并填充 struct file_operations * hello_fops. 按照 include\linux\fs.h 中的 struct file_operations来。ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);, ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);, int (*open) (struct inode *, struct file *);, int (*release) (struct inode *, struct file *);. static int hello_buf[1024]; // #define MIN(a, b) (a < b ? a : b) // static ssize_t hello_read (struct file * filp, char __user * buf, size_t count, loff_t * f_pos) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); int missing = copy_to_user(buf, hello_buf, MIN(1024, count)); return MIN(1024, count) - missing; } // static ssize_t hello_write (struct file * filp, const char __user * buf, size_t count, loff_t * f_pos) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); int missing = copy_from_user(hello_buf, buf, MIN(1024, count)); return MIN(1024, count) - missing; } // static int hello_open (struct inode * inode, struct file * filp) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } // static int hello_release (struct inode * inode, struct file * filp) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } // static struct file_operations hello_fops = { .owner = THIS_MODULE, .write = hello_write, .read = hello_read, .open = hello_open, .release = hello_release, }; 根据开发板情况，修改 Makefile KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88 根据编译情况，增加相关头文件 #include <linux/uaccess.h> 注意：int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops); 是旧的设备驱动。新的设备驱动可以用 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name); 配合 fs\char_dev.c include/linux/cdev.h 中的 void cdev_init(struct cdev *cdev, const struct file_operations *fops), int cdev_add(struct cdev *p, dev_t dev, unsigned count), 当然，设备卸载的时候，也需要对应的加上 void cdev_del(struct cdev *p), void unregister_chrdev_region(dev_t from, unsigned count) 注意: device_create 和 class_create 也不是必须的。最低要求其实就是 register_chrdev。 class_create 的用处是 /sys/class/ 下生成目录，并且方便作为设备类，统一管理多个设备，还可以与 udev mdev 配合使用。 device_create 的作用是，可以自动在 /dev 下面创建设备节点，如果不用，那就需要使用 mknod 来手动创建节点。 使用alloc_chrdev_region 的代码如下: static int __init hello_init(void) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); if (alloc_chrdev_region(&hello_devt, 0, 1, "hello") < 0) { printk("%s %s line, error %d\n", __FILE__, __FUNCTION__, __LINE__); return -1; } cdev_init(&hello_cdev, &hello_fops); if (cdev_add(&hello_cdev, hello_devt, 1) < 0) { printk("%s %s line, error %d\n", __FILE__, __FUNCTION__, __LINE__); unregister_chrdev_region(hello_devt, 1); return -1; } hello_class = class_create(THIS_MODULE, "hello"); if (IS_ERR(hello_class)) { printk("%s %s line %d, error %ld\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_class)); cdev_del(&hello_cdev); //unregister_chrdev(hello_major, "hello"); unregister_chrdev_region(hello_devt, 1); return PTR_ERR(hello_class); } hello_deivce = device_create(hello_class, NULL, hello_devt, NULL, "hello"); if (IS_ERR(hello_deivce)) { printk("%s %s line %d, error %ld\n", __FILE__, __FUNCTION__, __LINE__, PTR_ERR(hello_deivce)); class_destroy(hello_class); cdev_del(&hello_cdev); //unregister_chrdev(hello_major, "hello"); unregister_chrdev_region(hello_devt, 1); return PTR_ERR(hello_deivce); } return 0; } static void __exit hello_exit(void) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); device_destroy(hello_class, hello_devt); class_destroy(hello_class); cdev_del(&hello_cdev); //unregister_chrdev(hello_major, "hello"); unregister_chrdev_region(hello_devt, 1); } 1.4.3 测试 echo "7 4 1 7" > /proc/sys/kernel/printk 显示内核打印信息 insmod hello_drv.ko 安装驱动, 或者 insmod -f hello_drv.ko ls /dev/hello -l 查看设备节点 ./hello_drv_test -w www.100ask.net, ./hello_drv_test -r 测试 lsmod, rmmod hello_drv 第5章 最简单的 LED 驱动程序 5.2 最简单的 LED 驱动程序编程_基于 IMX6ULL 5.2.2 实现什么功能 使用 static inline void __iomem *ioremap(phys_addr_t offset, size_t size) 把 io 映射到内存中，然后就可以直接读写内存来驱动硬件。 需要包含头文件 #include <asm/io.h> 卸载时，用 iounmap 来取消映射。 使用 ioremap 映射的变量，也需要用 volatile 进行修饰。另外这些变量，如果考虑内存屏障，优化等问题，推荐使用 iowrite, ioread 这些函数。 第6章 LED 驱动程序框架 6.3 LED 驱动能支持多个板子的基础：分层思想 6.4 写代码 6.4.1 驱动程序 多个 led, 所以 init 和 exit 中，device_create 需要多个 for (i = 0; i < LED_NUM; i++) device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); /* /dev/100ask_led0,1,... */ for (i = 0; i < LED_NUM; i++) device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */ 多个 led，在 open, write 等中，需要针对某个 led 进行操作，只能使用次设备号来分辨。有 inode 时，直接 int minor = iminor(node); 来获取次设备号。没有 inode 有文件名时，通过 struct inode *inode = file_inode(file); 先获取 inode，再获取次设备号。 static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { ... struct inode *inode = file_inode(file); int minor = iminor(inode); ... /* 根据次设备号和status控制LED */ p_led_opr->ctl(minor, status); return 1; } // static int led_drv_open (struct inode *node, struct file *file) { int minor = iminor(node); ... /* 根据次设备号初始化LED */ p_led_opr->init(minor); return 0; } 第7章 具体单板的 LED 驱动程序 7.2 百问网 IMX6ULL 的 LED 驱动程序 7.2.3 写程序 从板级文件中获取 led 数目，然后 device_create static struct led_operations board_demo_led_opr = { .num = 1, .init = board_demo_led_init, .ctl = board_demo_led_ctl, }; p_led_opr = get_board_led_opr(); for (i = 0; i < p_led_opr->num; i++) device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); /* /dev/100ask_led0,1,... */ for (i = 0; i < p_led_opr->num; i++) device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */ 第8章 驱动设计的思想 8.3 分离 8.4 写示例代码 把具体的 gpio 放在一个 resource.h 的头文件中。这种方法适合于专门做模块的或者适配多种开发板。 对于普通做产品的，这种方法不合算。 第9章 驱动进化之路：总线设备驱动模型 9.2 在 Linux 中实现“分离”：Bus/Dev/Drv 模型 主要涉及 include\linux\platform_device.h 中的 struct platform_device, struct platform_driver, 和 include\linux\device.h 中的 struct bus_type. // include\linux\platform_device.h struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; char *driver_override; /* Driver name to force a match */ /* MFD cell pointer */ struct mfd_cell *mfd_cell; /* arch specific additions */ struct pdev_archdata archdata; }; // include\linux\platform_device.h struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver; const struct platform_device_id *id_table; bool prevent_deferred_probe; }; // include\linux\device.h struct bus_type { const char *name; const char *dev_name; struct device *dev_root; struct device_attribute *dev_attrs; /* use dev_groups instead */ const struct attribute_group **bus_groups; const struct attribute_group **dev_groups; const struct attribute_group **drv_groups; int (*match)(struct device *dev, struct device_driver *drv); int (*uevent)(struct device *dev, struct kobj_uevent_env *env); int (*probe)(struct device *dev); int (*remove)(struct device *dev); void (*shutdown)(struct device *dev); int (*online)(struct device *dev); int (*offline)(struct device *dev); int (*suspend)(struct device *dev, pm_message_t state); int (*resume)(struct device *dev); const struct dev_pm_ops *pm; const struct iommu_ops *iommu_ops; struct subsys_private *p; struct lock_class_key lock_key; }; 9.3 匹配规则 device 和 driver 匹配，按照优先顺序如下: 强行匹配: platform_device.driver_override = platform_driver.driver.name 列表匹配: platform_device.name = platform_driver.platform_device_id.name[] 名称匹配: platform_device.name = platform_driver.driver.name 9.3.4 函数调用关系 9.4 常用函数 9.4.1 注册/反注册 platform_device_register platform_device_unregister platform_driver_register platform_driver_unregister platform_add_devices 9.4.2 获得资源 struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) 返回该 dev 中某类型(type)资源中的第几个(num) int platform_get_irq(struct platform_device *dev, unsigned int num) 返回该 dev 所用的第几个(num)中断 struct resource *platform_get_resource_byname(struct platform_device *dev, unsigned int type, const char *name) 通过名字(name)返回该 dev 的某类型(type)资源 int platform_get_irq_byname(struct platform_device *dev， const char *name) 通过名字(name)返回该 dev 的中断号 第10章 LED 模板驱动程序的改造：总线设备驱动模型 10.2 要实现的框架 10.3 写代码 10.3.1 注意事项 platform_device.dev.release 必须提供，否则在卸载时，会有警告 供给其他模块调用的函数，也需要用 EXPORT_SYMBOL(), 并且模块需要先加载。 10.3.2 实现 platform_device 结构体 resource.start 可以用来存放数据， static struct resource resources[] = { { .start = GROUP_PIN(3,1), .flags = IORESOURCE_IRQ, .name = "100ask_led_pin", }, }; resources 元素个数，可以用 ARRAY_SIZE() static struct platform_device board_A_led_dev = { .name = "100ask_led", .num_resources = ARRAY_SIZE(resources), .resource = resources, .dev = { .release = led_dev_release, }, }; 加载和卸载函数，直接调用 platform_device_register(&board_A_led_dev), platform_device_unregister(&board_A_led_dev) 10.3.3 实现 platform_driver 结构体 platform_driver 很简单 static struct platform_driver chip_demo_gpio_driver = { .probe = chip_demo_gpio_probe, .remove = chip_demo_gpio_remove, .driver = { .name = "100ask_led", }, }; 加载，使用 platform_driver_register(&chip_demo_gpio_driver) 卸载，使用 platform_driver_unregister(&chip_demo_gpio_driver) chip_demo_gpio_probe 中 使用 platform_get_resource(pdev, IORESOURCE_IRQ, i++) 获取 device 中的 resource，然后调用 device_create 函数 chip_demo_gpio_remove 中使用 platform_get_resource(pdev, IORESOURCE_IRQ, i); 获取 resource ，然后调用 device_destroy 从框架上来看，device 是提供下层资源，driver 是实际的去添加设备，并且把相关的操作资源等等提供给上层的 led_drv. led_drv 做更加靠近上层的 open, write 之类的。 第11章 驱动进化之路：设备树的引入及简明教程 11.1 设备树的引入与作用 11.2.1 Devicetree 格式 DTS 文件的格式 /dts-v1/; // 表示版本 [memory reservations] // 格式为: /memreserve/ <length>; / { [property definitions] [child nodes] }; node 的格式 [label:] node-name[@unit-address] { [properties definitions] [child nodes] }; /dts-v1/; / { // label 是标号，可以省略。label 的作用是为了方便地引用 node uart0: uart@fe001000 { compatible="ns16550"; reg=<0xfe001000 0x100>; }; }; // 在根节点之外使用 label 引用 node： &uart0 { status = “disabled”; }; // 或在根节点之外使用全路径： &{/uart@fe001000} { status = “disabled”; }; properties 的格式 [label:] property-name = value; [label:] property-name; Property 取值只有 3 种, arrays of cells(1 个或多个 32 位数据, 64 位数据使用 2 个 32 位数据表示), string(字符串), bytestring(1 个或多个字节) // Arrays of cells : cell 就是一个 32 位的数据，用尖括号包围起来 interrupts = <17 0xc>; // 64bit 数据使用 2 个 cell 来表示，用尖括号包围起来: clock-frequency = <0x00000001 0x00000000>; // A null-terminated string (有结束符的字符串)，用双引号包围起来 compatible = "simple-bus"; // A bytestring(字节序列) ，用中括号包围起来: local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示 local-mac-address = [000012345678]; // 每个 byte 使用 2 个 16 进制数来表示 // 可以是各种值的组合, 用逗号隔开: compatible = "ns16550", "ns8250"; example = <0xf00f0000 19>, "a strange property format"; 11.2.2 dts 文件包含 dtsi 文件 设备树文件不需要我们从零写出来，内核支持了某款芯片比如 imx6ull，在内核的 arch/arm/boot/dts 目录下就有了能用的设备树模板，一般命名为 xxxx.dtsi。“i”表示“include”，被别的文件引用的。 dtsi 文件跟 dts 文件的语法是完全一样的 dts 中可以包含.h 头文件，也可以包含 dtsi 文件，在.h 头文件中可以定义一些宏。 /dts-v1/; #include <dt-bindings/input/input.h> #include "imx6ull.dtsi" // / { …… }; 11.2.3 常用的属性 #address-cells、#size-cells 地址和大小. address 要用多少个 32 位数来表示, size 要用多少个 32 位数来表示 // address-cells 为 1，所以 reg 中用 1 个数来表示地址，即用 0x80000000 来表示地址； // size-cells 为 1，所以 reg 中用 1 个数来表示大小，即用 0x20000000 表示大小 / { #address-cells = <1>; #size-cells = <1>; memory { reg = <0x80000000 0x20000000>; }; }; “compatible”表示“兼容”，对于某个 LED，内核中可能有 A、B、C 三个驱动都支持它，那可以这样写 // compatible 的值，建议取这样的形式："manufacturer,model"，即“厂家名，模块名” led { compatible = “A”, “B”, “C”; }; model 用来准确地定义这个硬件是什么。 { compatible = "samsung,smdk2440", "samsung,mini2440"; model = "jz2440_v3"; }; dtsi 文件中定义了很多设备，但是在你的板子上某些设备是没有的。这时你可以给这个设备节点添加一个 status 属性，设置为“disabled” // okay, disabled, fail, fail-xxx &uart1 { status = "disabled"; }; reg 用来表示寄存器地址, 在设备树里，它可以用来描述一段空间。反正对于 ARM 系统，寄存器和内存是统一编址的，即访问寄存器时用某块地址，访问内存时用某块地址，在访问方法上没有区别。reg 属性的值，是一系列的“address size”，用多少个 32 位的数来表示address 和 size，由其父节点的#address-cells、#size-cells 决定。 /dts-v1/; / { #address-cells = <1>; #size-cells = <1>; memory { reg = <0x80000000 0x20000000>; }; }; name(过时了，建议不用)在跟 platform_driver 匹配时，优先级最低。compatible 属性在匹配过程中，优先级最高。 device_type(过时了，建议不用)它的值是字符串，用来表示节点的类型。在跟 platform_driver 匹配时，优先级为中。 11.2.4 常用的节点(node) 根节点 dts 文件中必须有一个根节点 /dts-v1/; / { model = "SMDK24440"; compatible = "samsung,smdk2440"; #address-cells = <1>; #size-cells = <1>; }; CPU 节点 一般不需要我们设置，在 dtsi 文件中都定义好了 memory 节点, 芯片厂家不可能事先确定你的板子使用多大的内存，所以 memory 节点需要板厂设置 memory { reg = <0x80000000 0x20000000>; }; chosen 节点, 我们可以通过设备树文件给内核传入一些参数，这要在 chosen 节点中设置 bootargs 属性 chosen { bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200"; }; 11.3 编译、更换设备树 11.3.1 在内核中直接 make 内核源码的目录 make dtbs V=1 11.3.2 手工编译 除非你对设备树比较了解，否则不建议手工使用 dtc 工具直接编译。内核目录下 scripts/dtc/dtc 是设备树的编译工具，直接使用它的话，包含其他文件时不能使用“#include”，而必须使用“/incldue”. 编译、反编译的示例命令如下，“-I”指定输入格式，“-O”指定输出格式，“-o”指定输出文件 ./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts // 编译 dts 为 dtb ./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb // 反编译 dtb 为dts 11.3.3 给开发板更换设备树文件 内核源码目录中执行 make dtbs 11.3.4 板子启动后查看设备树 ls /sys/firmware/ /sys/firmware/devicetree 目录下是以目录结构程现的 dtb 文件, 根节点对应 base 目录, 每一个节点对应一个目录, 每一个属性对应一个文件。属性的值如果是字符串，可以使用 cat 命令把它打印出来；对于数值，可以用 hexdump 把它打印出来。 /sys/firmware/fdt 文件，它就是 dtb 格式的设备树文件，可以把它复制出来放到 ubuntu 上，执行下面的命令反编译出来(-I dtb：输入格式是 dtb，-O dts：输出格式是 dts) cd 板子所用的内核源码目录 ./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp.dts 11.4 内核对设备树的处理 dts 在 PC 机上被编译为 dtb 文件； u-boot 把 dtb 文件传给内核； 内核解析 dtb 文件，把每一个节点都转换为 device_node 结构体； 对于某些 device_node 结构体，会被转换为 platform_device 结构体。 11.4.1 dtb 中每一个节点都被转换为 device_node 结构体 数值保存在 device_node.property 根节点被保存在全局变量 of_root 中，从 of_root 开始可以访问到任意节点。 11.4.2 哪些设备树节点会被转换为 platform_device 根节点下含有 compatile 属性的子节点 含有特定 compatile 属性的节点的子节点 总线 I2C、SPI 节点下的子节点：不转换为 platform_device 11.4.3 怎么转换为 platform_device platform_device 中含有 resource 数组, 它来自 device_node 的 reg, interrupts 属性; platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性 11.5 platform_device 如何与 platform_driver 配对 从设备树转换得来的 platform_device 会被注册进内核里，以后当我们每注册一个 platform_driver 时，它们就会两两确定能否配对，如果能配对成功就调用 platform_driver 的 probe 函数。 // drivers\base\platform.c static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev = to_platform_device(dev); struct platform_driver *pdrv = to_platform_driver(drv); /* When driver_override is set, only bind to the matching driver */ if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); /* Attempt an OF style match first */ if (of_driver_match_device(dev, drv)) return 1; /* Then try ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; /* Then try to match against the id table */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* fall-back to driver name match */ return (strcmp(pdev->name, drv->name) == 0); } 从上面的源码，可以很明显的看出，设备树是第二个优先级的匹配。 11.5.2 然后比较：设备树信息 platform_device.dev.of_node = platform_driver.driver.of_match_table // platform_device.dev.of_node // include\linux\of.h struct device_node { const char *name; const char *type; // device_type ... struct property *properties; // compatible }; // platform_driver.driver.of_match_table // include\linux\mod_devicetable.h struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; }; 首先，比较 compatible, 然后是 type, 最后是 name. 设备树中建议不再使用 devcie_type 和 name 属性，所以基本上只使用设备节点的 compatible 属性来寻找匹配的 platform_driver。 11.7 内核里操作设备树的常用函数 内核源码中 include/linux/目录下有很多 of 开头的头文件，of 表示“open firmware”即开放固件。 11.7.1 内核中设备树相关的头文件介绍 设备树的处理过程是：dtb -> device_node -> platform_device。 处理 DTB of_fdt.h // dtb 文件的相关操作函数, 我们一般用不到, // 因为 dtb 文件在内核中已经被转换为 device_node 树(它更易于使用) 处理 device_node of.h // 提供设备树的一般处理函数, // 比如 of_property_read_u32(读取某个属性的 u32 值), // of_get_child_count(获取某个 device_node 的子节点数) of_address.h // 地址相关的函数, // 比如 of_get_address(获得 reg 属性中的 addr, size 值) // of_match_device (从 matches 数组中取出与当前设备最匹配的一项) of_dma.h // 设备树中 DMA 相关属性的函数 of_gpio.h // GPIO 相关的函数 of_graph.h // GPU 相关驱动中用到的函数, 从设备树中获得 GPU 信息 of_iommu.h // 很少用到 of_irq.h // 中断相关的函数 of_mdio.h // MDIO (Ethernet PHY) API of_net.h // OF helpers for network devices. of_pci.h // PCI 相关函数 of_pdt.h // 很少用到 of_reserved_mem.h // reserved_mem 的相关函数 处理 platform_device of_platform.h // 把 device_node 转换为 platform_device 时用到的函数, // 比如 of_device_alloc(根据 device_node 分配设置 platform_device), // of_find_device_by_node (根据 device_node 查找到 platform_device), // of_platform_bus_probe (处理 device_node 及它的子节点) of_device.h // 设备相关的函数, 比如 of_match_device 11.7.2 platform_device 相关的函数 of_find_device_by_node extern struct platform_device *of_find_device_by_node(struct device_node *np); 设备树中的每一个节点，在内核里都有一个 device_node；你可以使用device_node 去找到对应的 platform_device。 platform_get_resource struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) 这个函数跟设备树没什么关系， 但是设备树中的节点被转换为platform_device 后，设备树中的 reg 属性、interrupts 属性也会被转换为“resource”. 这时，你可以使用这个函数取出这些资源。 // 对于设备树节点中的 reg 属性，它对应 IORESOURCE_MEM 类型的资源 // 对于设备树节点中的 interrupts 属性，它对应 IORESOURCE_IRQ 类型的资源。 11.7.3 有些节点不会生成 platform_device，怎么访问它们 找到节点 , of_find_node_by_path of_find_node_by_name of_find_node_by_type of_find_compatible_node of_find_node_by_phandle of_get_parent of_get_next_parent of_get_next_child of_get_next_available_child of_get_child_by_name 找到属性, of_find_property struct property *of_find_property(const struct device_node *np, const char *name, int *lenp); 获取属性的值, of_get_property of_property_count_elems_of_size of_property_read_u32 of_property_read_u64 of_property_read_u32_index of_property_read_variable_u8_array of_property_read_variable_u16_array of_property_read_variable_u32_array of_property_read_variable_u64_array of_property_read_string 11.8 怎么修改设备树文件 有些芯片，厂家提供了对应的设备树生成工具，可以选择某个引脚用于某些功能，就可以自动生成设备树节点。 参考内核文档 Documentation/devicetree/bindings/ 第12章 LED 模板驱动程序的改造：设备树 12.2 怎么使用设备树写驱动程序 12.2.1 设备树节点要与 platform_driver 能匹配 12.2.2 设备树节点指定资源，platform_driver 获得资源 如果在设备树节点里使用 interrupts 属性 ， 那么内核生成对应的 platform_device 时会用 reg 属性来设置 IORESOURCE_IRQ 类型的资源。对于interrupts 属性，内核会检查它的有效性，所以不建议在设备树里使用该属性来表示其他资源。 可以从 platform_device 中得到 device_node，再用 of_property_read_u32 得到属性的值 #define GROUP_PIN(g,p) ((g<<16) | (p)) 100ask_led0 { compatible = “100ask,led”; pin = <GROUP_PIN(5, 3)>; }; struct device_node* np = pdev->dev. of_node; int led_pin; int err = of_property_read_u32(np, “pin”, &led_pin); 12.3 开始编程 12.3.1 修改设备树添加 led 设备节点 ~/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts linux 编译dts 受控于arch/<arch>/boot/dts/Makefile 里面的 dtb-$(CONFIG_<SOC>), dtb-$(CONFIG_<SOC>) 需要去 .config 里面去查看。使用 make dtbs 会编译所有使能的 dts。编译后的 dtb 文件在 arch/<arch>/boot/dts/ 中。 如果想要单独编译某个 dts, make arch/arm/boot/dts/sun8i-h3-nanopi-neo.dtb #define GROUP_PIN(g,p) ((g<<16) | (p)) model = "Freescale i.MX6 ULL 14x14 EVK Board ptz"; 100ask_led@0 { compatible = "100ask,leddrv"; pin = <GROUP_PIN(3, 1)>; }; // 100ask_led@1 { compatible = "100ask,leddrv"; pin = <GROUP_PIN(5, 8)>; }; 修改之后，make dtbs 编译设备树 查看当前设备加载的设备树，dmesg | grep "OF: fdt:" 可以看到设备树的 model 信息。 备份好 /boot 下面原来的 dtb，然后把新修改的 dtb 复制替换。 reboot 然后，再用 dmesg | grep "OF: fdt:" 查看，是否加载新的 dtb 了。 12.3.2 修改 platform_driver 的源码 static const struct of_device_id ask100_leds[] = { { .compatible = "100ask,leddrv" }, { }, }; // static struct platform_driver chip_demo_gpio_driver = { .probe = chip_demo_gpio_probe, .remove = chip_demo_gpio_remove, .driver = { .name = "100ask_led", .of_match_table = ask100_leds, }, }; probe remove 函数中，也修改为从设备树获取。而且因为每匹配一次，就 probe 一下，所以 probe 里面的 while 循环也可以去掉了。 remove 也是一样。 static int chip_demo_gpio_probe(struct platform_device *pdev) { struct device_node *np; int err = 0; int led_pin; np = pdev->dev.of_node; if (!np) return -1; err = of_property_read_u32(np, "pin", &led_pin); g_ledpins[g_ledcnt] = led_pin; led_class_create_device(g_ledcnt); g_ledcnt++; return 0; } 第15章 具体单板的按键驱动程序(查询方式) 15.2 百问网 IMX6ULL 的按键驱动程序(查询方式) 15.2.3 编程 gpio 相关的放结构体中，ioremap 针对整个结构体。 第16章 GPIO 和 Pinctrl 子系统的使用 内核 Documentation\devicetree\bindings\Pinctrl\Pinctrl-bindings.txt 内核 Documentation\gpio\Pinctrl-bindings.txt 内核 Documentation\devicetree\bindings\gpio\gpio.txt 16.1 Pinctrl 子系统重要概念 16.1.1 引入 Pinctrl 管理引脚复用和配置 gpio 管理 gpio 模块。 16.1.2 重要概念 内核 Documentation\devicetree\bindings\Pinctrl\Pinctrl-bindings.txt pin controller 用它来复用引脚、配置引脚。它是一个软件上的概念，你可以认为它对应 IOMUX──用来复用引脚，还可以配置引脚(比如上下拉电阻等)。 pin controller 和 GPIO Controller 不是一回事, 先用 pin controller 把引脚配置为 GPIO，再用 GPIO Controler 把引脚配置为输入或输出。 client device 声明自己要使用哪些引脚的哪些功能，怎么配置它们。就是使用 Pinctrl 系统的设备，使用引脚的设备。它在设备树里会被定义为一个节点，在节点里声明要用哪些引脚。 pin state, 对于一个“client device”来说，比如对于一个 UART 设备，它有多个“状态”：default、sleep 等，那对应的引脚也有这些状态。 groups 和 function：一个设备会用到一个或多个引脚，这些引脚就可以归为一组(group)； 这些引脚可以复用为某个功能：function。 Generic pin multiplexing node 和 Generic pin configuration node , 可 以 用 来 描 述 复 用 信 息 ： 哪 组 (group) 引 脚 复 用 为 哪 个 功 能(function)； 可以用来描述配置信息：哪组(group)引脚配置为哪个设置功能(setting)，比如上拉、下拉等。pin controller 节点的格式，没有统一的标准！！！！每家芯片都不一样。 甚至上面的 group、function 关键字也不一定有，但是概念是有的。 16.1.4 代码中怎么引用 pinctrl 这是透明的，我们的驱动基本不用管。当设备切换状态时，对应的 pinctrl就会被调用。非要自己调用，也有函数： devm_pinctrl_get_select_default(struct device *dev); // 使用"default"状态的引脚 pinctrl_get_select(struct device *dev, const char *name); // 根据 name 选择某种状态的引脚 pinctrl_put(struct pinctrl *p); // 不再使用, 退出时调用 16.2 GPIO 子系统重要概念 16.2.1 引入 在设备树里指定 GPIO 引脚 在驱动代码中：使用 GPIO 子系统的标准函数获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值。 16.2.2 在设备树中指定引脚 “GPIO 组”就是一个 GPIO Controller，这通常都由芯片厂家设置好。我们要做的是找到它名字，比如“gpio1”，然后指定要用它里面的哪个引脚，比如<&gpio1 0>。dtsi 定义，dts 中使用。 gpio1: gpio@0209c000 { compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; reg = <0x0209c000 0x4000>; interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; }; &usdhc1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_usdhc1>; cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>; keep-power-in-suspend; enable-sdio-wakeup; bus-width = <4>; status = "okay"; }; 我们暂时只需要关心里面的这 2 个属性, “gpio-controller” 表示这个节点是一个 GPIO Controller，它下面有很 多引脚。 “#gpio-cells = ” 表示这个控制器下每一个引脚要用 2 个 32 位的数(cell)来描述。普遍的用法是，用第 1 个 cell 来表示哪一个引脚，用第 2 个 cell 来表示有效电平 16.2.3 在驱动代码中调用 GPIO 子系统 GPIO 子系统有两套接口：基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“gpiod_”，它使用 gpiodesc 结构体来表示一个引脚；后者的函数都有前缀“gpio”，它使用一个整数来表示一个引脚。 驱动程序中要包含头文件, gpiod 或 gpio #include <linux/gpio/consumer.h> // descriptor-based #include <linux/gpio.h> // legacy 常用函数 获得 GPIO descriptor-based legacy gpiod_get gpio_request gpiod_get_index gpiod_get_array gpio_request_array devm_gpiod_get devm_gpiod_get_index devm_gpiod_get_array 常用函数 设置方向 descriptor-based legacy gpiod_direction_input gpio_direction_input gpiod_direction_output gpio_direction_output 常用函数 读值、写值 descriptor-based legacy gpiod_get_value gpio_get_value gpiod_set_value gpio_set_value 常用函数 释放 GPIO descriptor-based legacy gpio_free gpio_free gpiod_put gpio_free_array gpiod_put_array devm_gpiod_put devm_gpiod_put_array 有前缀“devm”的含义是“设备资源管理”(Managed Device Resource)，这是一种自动释放资源的机制。它的思想是“资源是属于设备的，设备不存在时资源就可以自动释放”。如果使用 devm 的相关函数，在内存申请失败时可以直接返回：设备的销毁函数会自动地释放已经申请了的 GPIO 资源。建议使用“devm”版本的相关函数。 使用下面的函数获得引脚例子 foo_device { compatible = "acme,foo"; ... led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */ <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */ <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */ power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>; }; struct gpio_desc *red, *green, *blue, *power; // red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH); green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH); blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH); power = gpiod_get(dev, "power", GPIOD_OUT_HIGH); 注意：gpiod_set_value 设置的值是“逻辑值”，不一定等于物理值。如何设备树中设置为低电平使能，那么 gpiod_set_value(desc, 0) 会让实际的电平为高电平。 旧的“gpio_”函数没办法根据设备树信息获得引脚，它需要先知道引脚号。 16.2.4 sysfs 中的访问方法 在 sysfs 中访问 GPIO，实际上用的就是引脚号，老的方法。先确定某个 GPIO Controller 的基准引脚号(base number)，再计算出某个引脚的号码。 先在开发板的 /sys/class/gpio 目录下，找到各个 gpiochipXXX 目录 然后进入某个 gpiochip 目录，查看文件 label 的内容 根据 label 的内容对比设备树，比如 gpiochip96 的 label 为 20a800.gpio， 设备树中 gpio4: gpio@20a800. 所以，通过 20a800 把 gpio4 和 96 联系起来，gpio4 的基准引脚号就是 96，可以通过 base 内容核对。 GPIO4_14 的引脚号码是 96+14=110，相关操作是: // 输入引脚 [root@100ask:~]# echo 110 > /sys/class/gpio/export [root@100ask:~]# echo in > /sys/class/gpio/gpio110/direction [root@100ask:~]# cat /sys/class/gpio/gpio110/value [root@100ask:~]# echo 110 > /sys/class/gpio/unexport // 输出引脚，假设引脚号为 N，可以用下面的方法设置它的值为 1： [root@100ask:~]# echo N > /sys/class/gpio/export [root@100ask:~]# echo out > /sys/class/gpio/gpioN/direction [root@100ask:~]# echo 1 > /sys/class/gpio/gpioN/value [root@100ask:~]# echo N > /sys/class/gpio/unexport 16.3 基于 GPIO 子系统的 LED 驱动程序 16.3.1 编写思路 16.3.3 在设备树中添加 GPIO 信息 在设备树中指定：添加”[name]-gpios”属性，指定使用的是哪一个 GPIO Controller 里的哪一个引脚，还有其他 Flag 信息，比如 GPIO_ACTIVE_LOW 等。 具体需要多少个 cell 来描述一个引脚，需要查看设备树中这个 GPIO Controller 节点里的“#gpio-cells”属性值，也可以查看内核文档。 16.4 在 100ASK_IMX6ULL 上机实验 16.4.1 确定引脚并生成设备树节点 NXP 公司对于 IMX6ULL 芯片，有设备树生成工具。 Pinctrl 信息： &iomuxc_snvs { …… myled_for_gpio_subsys: myled_for_gpio_subsys{ fsl,pins = < MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x000110A0 >; }; 设备节点信息(放在根节点下)： myled { compatible = "100ask,leddrv"; pinctrl-names = "default"; pinctrl-0 = <&myled_for_gpio_subsys>; led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; }; 16.4.2 编译程序 /sys/firmware/devicetree/base/led_100ask 这个和设备树中，新的设备节点名称对应 led_100ask { compatible = "100ask,leddrv"; pinctrl-names = "default"; pinctrl-0 = <&gpio_leds_100ask>; led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; }; /sys/class/100ask_led_class/100ask_led0 /sys/devices/soc0/led_100ask /sys/devices/soc0/led_100ask/driver /sys/devices/soc0/led_100ask/driver/led_100ask/of_node 第18章 Linux 系统对中断的处理 18.2 Linux 系统对中断处理的演进 Linux 系统中有硬件中断，也有软件中断。对硬件中断的处理有 2 个原则：不能嵌套，越快越好。 18.2.1 Linux 对中断的扩展：硬件中断、软件中断 // include\linux\interrupt.h enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, IRQ_POLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */ RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS }; 触发软件中断 void raise_softirq(unsigned int nr); 设置软件中断的处理函数 void open_softirq(int nr, void (*action)(struct softirq_action *)); 18.2.4 要处理的事情实在太多，拆分为：上半部、下半部 在 handler 函数里只做紧急的事，然后就重新开中断，让系统得以正常运行；那些不紧急的事，以后再处理，处理时是开中断的。 中断下半部的实现有很多种方法，讲 2 种主要的：tasklet(小任务)、work queue(工作队列)。 18.2.5 下半部要做的事情耗时不是太长：tasklet 硬件中断 A 处理过程中，没有其他中断发生：上述流程图①～⑨依次执行，上半部、下半部的代码各执行一次。 硬件中断 A 处理过程中，又再次发生了中断 A：同一个中断的上半部、下半部，在执行时是多对一的关系。 硬件中断 A 处理过程中，又再次发生了中断 B：多个中断的下半部，是汇集在一起处理的。 总结: 中断上半部，用来处理紧急的事，它是在关中断的状态下执行的 中断下半部，用来处理耗时的、不那么紧急的事，它是在开中断的状态下执行的 中断下半部执行时，有可能会被多次打断，有可能会再次发生同一个中断 中断上半部执行完后，触发中断下半部的处理 中断上半部、下半部的执行过程中，不能休眠 18.2.6 下半部要做的事情太多并且很复杂：工作队列 如果中断要做的事情实在太耗时，那就不能用软件中断来做，而应该用内核线程来做：在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行，APP 有机会执行，系统不会卡顿。 这个内核线程是系统帮我们创建的，一般是 kworker 线程，内核中有很多这样的线程, 使用 ps -A | grep kworker 可以查看。kworker 线程要去“工作队列”(work queue)上取出一个一个“工作”(work)，来执行它里面的函数。 创建 work：static DECLARE_WORK(pmac_backlight_key_work, pmac_backlight_key_worker); 类似这样，前面是结构体，后面是函数 把 work 提交给 work queue , schedule_work(&pmac_backlight_key_work); 会把 work 提供给系统默认的 work queue：system_wq，它是一个队列。 schedule_work 函数不仅仅是把 work 放入队列，还会把kworker 线程唤醒。此线程抢到时间运行时，它就会从队列中取出 work，执行里面的函数。 在中断场景中，可以在中断上半部调用 schedule_work 函数。 总结： 很耗时的中断处理，应该放到线程里去 可以使用 work、work queue 在中断上半部调用 schedule_work 函数，触发 work 的处理 既然是在线程中运行，那对应的函数可以休眠。 18.2.7 新技术：threaded irq 你可以只提供 thread_fn，系统会为这个函数创建一个内核线程。发生中断时，内核线程就会执行这个函数。 int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) , handler 是上半部，thread_fn 是下半部。 用 work 来线程化地处理中断，一个 worker 线程只能由一个 CPU 执行，多个中断的 work 都由同一个 worker 线程来处理，在单 CPU 系统中也只能忍着了。但是在 SMP 系统中，明明有那么多 CPU 空着，你偏偏让多个中断挤在这个CPU 上？ threaded irq，为每一个中断都创建一个内核线程；多个中断的内核线程可以分配到多个 CPU 上执行，这提高了效率。 18.3 Linux 中断系统中的重要数据结构 最核心的结构体是 irq_desc，之前为了易于理解，我们说在 Linux 内核中有一个中断数组，对于每一个硬件中断，都有一个数组项，这个数组就是irq_desc 数组。 如果内核配置了 CONFIG_SPARSE_IRQ，那么它就会用基数树(radix tree) 来代替 irq_desc 数组。 18.3.1 irq_desc 数组 irq_desc 结构体在 include/linux/irqdesc.h 中定义 struct irq_desc { ... struct irq_data irq_data; ... irq_flow_handler_t handle_irq; ... struct irqaction *action; /* IRQ action list */ ... const char *name; } ____cacheline_internodealigned_in_smp; 每一个 irq_desc 数组项中都有一个函数：handle_irq，还有一个 action 链表。 外部设备 1、外部设备 n 共享一个 GPIO 中断 B，多个 GPIO 中断汇聚到GIC(通用中断控制器)的 A 号中断，GIC 再去中断 CPU。那么软件处理时就是反 过来，先读取 GIC 获得中断号 A，再细分出 GPIO 中断 B，最后判断是哪一个外部芯片发生了中断。 GIC 的处理函数：假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家)，这个函数需要读取芯片的 GPIO 控制器，细分发生的是哪一个 GPIO 中断(假设是B)，再去调用 irq_desc[B]. handle_irq。 模块的中断处理函数： 对 于 GPIO 模 块 向 GIC 发 出 的 中 断 B ， 它 的 处 理 函 数 irq_desc[B].handle_irq。 导致 GPIO 中断 B 发生的原因很多，所以 irq_desc[B].handle_irq 会调用某个链表里的函数，这些函数由外部设备提供。这些函数自行判断该中断是否自己产生，若是则处理。 外部设备提供的处理函数：处理函数由自己驱动程序提供，对于共享中断，比如 GPIO 中断 B，它的中断来源可能有多个，每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表，存放着多个中断源的处理函数。 一旦程序确定发生了 GPIO 中断 B，那么就会从链表里把那些函数取出来，一一执行。这个链表就是 action 链表。 18.3.2 irqaction 结构体 irqaction 结构体在 include/linux/interrupt.h 中定义 struct irqaction { irq_handler_t handler; void *dev_id; ... struct irqaction *next; irq_handler_t thread_fn; struct task_struct *thread; struct irqaction *secondary; unsigned int irq; unsigned int flags; ... const char *name; ... } ____cacheline_internodealigned_in_smp; 当调用 request_irq、request_threaded_irq 注册中断处理函数时，内核就会构造一个 irqaction 结构体。在里面保存 name、dev_id 等，最重要的是 handler、thread_fn、thread。 handler 是中断处理的上半部函数，用来处理紧急的事情。thread_fn 对应一个内核线程 thread，当 handler 执行完毕，Linux 内核会唤醒对应的内核线程。在内核线程里，会调用 thread_fn 函数。 可以提供 handler 而不提供 thread_fn，就退化为一般的 request_irq 函数。 可以不提供 handler 只提供 thread_fn，完全由内核线程来处理中断。 也可以既提供 handler 也提供 thread_fn，这就是中断上半部、下半部。 在 reqeust_irq 时可以传入 dev_id，在共享中断中必须提供 dev_id，非共享中断可以不提供。 中断处理函数执行时，可以使用 dev_id 卸载中断时要传入 dev_id，这样才能在 action 链表中根据 dev_id 找到对应项 18.3.3 irq_data 结构体 irq_data 结构体在 include/linux/irq.h 中定义 struct irq_data { u32 mask; unsigned int irq; unsigned long hwirq; struct irq_common_data *common; struct irq_chip *chip; struct irq_domain *domain; #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY struct irq_data *parent_data; #endif void *chip_data; }; 它就是个中转站，里面有 irq_chip 指针 irq_domain 指针，都是指向别的结构体。 irq 是软件中断号，hwirq 是硬件中断号。 联系由 irq_domain 来建立。irq_domain会把本地的 hwirq 映射为全局的 irq， 18.3.4 irq_domain 结构体 irq_domain 结构体在 include/linux/irqdomain.h 中定义， struct irq_domain { struct list_head link; const char *name; const struct irq_domain_ops *ops; void *host_data; unsigned int flags; /* Optional data */ struct fwnode_handle *fwnode; enum irq_domain_bus_token bus_token; struct irq_domain_chip_generic *gc; #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY struct irq_domain *parent; #endif /* reverse map data. The linear map gets appended to the irq_domain */ irq_hw_number_t hwirq_max; unsigned int revmap_direct_max_irq; unsigned int revmap_size; struct radix_tree_root revmap_tree; unsigned int linear_revmap[]; }; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_RISING>; 表示要使用 gpio1 里的第 5 号中断，hwirq 就是 5。但是我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断，irq 是什么？它是软件中断号，它应该从“gpio1 的第 5 号中断”转换得来。 irq_domain 结构体中有一个 irq_domain_ops 结构体，里面有各种操作函数，主要是： xlate 用来解析设备树的中断属性，提取出 hwirq、type 等信息。 map 把 hwirq 转换为 irq。 18.3.5 irq_chip 结构体 irq_chip 结构体在 include/linux/irq.h 中定义 struct irq_chip { ... void (*irq_enable)(struct irq_data *data); void (*irq_disable)(struct irq_data *data); void (*irq_ack)(struct irq_data *data); void (*irq_mask)(struct irq_data *data); void (*irq_mask_ack)(struct irq_data *data); ... }; 我们在 request_irq 后，并不需要手工去使能中断，原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。 我们提供的中断处理函数中，也不需要执行主芯片相关的清中断操作，也是系统帮我们调用 irq_chip 中的相关函数。 但是对于外部设备相关的清中断操作，还是需要我们自己做的。 18.4 在设备树中指定中断_在代码中获得中断 18.4.1 设备树里中断节点的语法 内核 Documentation\devicetree\bindings\interrupt-controller\interrupts.txt 设备树里的中断控制器 在设备树中，中断控制器节点中必须有一个属性：interrupt-controller，表明它是“中断控制器”。 还必须有一个属性：#interrupt-cells，表明引用这个中断控制器的话需要多少个 cell。 #interrupt-cells=<1> 别的节点要使用这个中断控制器时，只需要一个 cell 来表明使用“哪一个中断”。 #interrupt-cells=<2> 别的节点要使用这个中断控制器时，需要一个 cell 来表明使用“哪一个中断”； 还需要另一个 cell 来描述中断，一般是表明触发类型： 第 2 个 cell 的 bits[3:0] 用来表示中断触发类型(trigger type and level flags)： 1 = low-to-high edge triggered，上升沿触发 2 = high-to-low edge triggered，下降沿触发 4 = active high level-sensitive，高电平触发 8 = active low level-sensitive，低电平触发 如 果 中 断 控 制 器 有 级 联 关 系 ， 下 级 的 中 断 控 制 器 还 需 要 表 明 它 的“ interrupt-parent ” 是 谁 ， 用 了 interrupt-parent ” 中 的 哪 一 个“interrupts”， 设备树里使用中断 interrupt-parent=<&XXXX> 你要用哪一个中断控制器里的中断？ interrupts 你要用哪一个中断？Interrupts 里要用几个 cell，由 interrupt-parent 对应的中断控制器决定。 i2c@7000c000 { gpioext: gpio-adnp@41 { compatible = "ad,gpio-adnp"; // interrupt-parent = <&gpio>; interrupts = <160 1>; // gpio-controller; #gpio-cells = <1>; // interrupt-controller; #interrupt-cells = <2>; }; ...... }; 新写法：interrupts-extended, 一个“interrupts-extended”属性就可以既指定“interrupt-parent”，也指定“interrupts”， interrupts-extended = <&intc1 5 1>, <&intc2 1 0>; 18.4.3 在代码中获得中断 对于 platform_device, /** * platform_get_resource - get a resource for a device * @dev: platform device * @type: resource type // 取哪类资源？IORESOURCE_MEM、IORESOURCE_REG * // IORESOURCE_IRQ 等 * @num: resource index // 这类资源中的哪一个？ */ struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num); 对于 I2C 设备、SPI 设备. I2C 总线驱动在处理设备树里的 I2C 子节点时，也会处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体，中断号会保存在 i2c_client 的 irq 成员里，代码如下(drivers/i2c/i2c-core.c). 对于 SPI 设备节点，SPI 总线驱动在处理设备树里的 SPI 子节点时，也会处理其中的中断信息。一个 SPI 设备会被转换为一个 spi_device 结构体，中断号会保存在 spi_device 的 irq 成员里，代码如下(drivers/spi/spi.c). 调用 of_irq_get 获得中断号, 如果你的设备节点既不能转换为 platform_device，它也不是 I2C 设备，不是 SPI 设备，那么在驱动程序中可以自行调用 of_irq_get 函数去解析设备树，得到中断号。 对于 GPIO, 参考：drivers/input/keyboard/gpio_keys.c 可以使用 gpio_to_irq 或 gpiod_to_irq 获得中断号。 gpio-keys { compatible = "gpio-keys"; pinctrl-names = "default"; user { label = "User Button"; gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>; gpio-key,wakeup; linux,code = <KEY_1>; }; }; // 可以使用下面的函数获得引脚和 flag button->gpio = of_get_gpio_flags(pp, 0, &flags); bdata->gpiod = gpio_to_desc(button->gpio); // 再去使用 gpiod_to_irq 获得中断号： irq = gpiod_to_irq(bdata->gpiod); 18.5 编写使用中断的按键驱动程序 对于 GPIO 按键，我们并不需要去写驱动程序，使用内核自带的驱动程序 drivers/input/keyboard/gpio_keys.c 就可以，然后你需要做的只是修改设备树指定引脚及键值。 当然，也可以从头写按键驱动，特别是如何使用中断。 18.5.2 先编写驱动程序 使用自定义结构体来保存 gpio 数据 struct gpio_key{ int gpio; struct gpio_desc *gpiod; int flag; int irq; } ; 从设备树获得 GPIO count = of_gpio_count(node); 申请自定义 gpio 结构体内存 gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL); 获取 gpio 号和 flag， gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag); 从 GPIO 获得中断号 gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio); 准备申请中断的 flag gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW; if (flag & OF_GPIO_ACTIVE_LOW) flags |= GPIOF_ACTIVE_LOW; 申请 gpio 资源 err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL); 保存 irq gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio); 申请中断, 并提供中断函数的参数 `err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]); 中断函数 static irqreturn_t gpio_key_isr(int irq, void *dev_id) { struct gpio_key *gpio_key = dev_id; int val; val = gpiod_get_value(gpio_key->gpiod); printk("key %d %d\n", gpio_key->gpio, val); return IRQ_HANDLED; } remove 的时候，别忘了 free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]); kfree(gpio_keys_100ask); 第19章 驱动程序基石 19.1 休眠与唤醒 19.1.2 内核函数 休眠函数 参考内核源码：include\linux\wait.h ， wq：waitqueue，等待队列， 休眠时除了把程序状态改为非 RUNNING 之外，还要把进程/进程放入 wq 中，以后中断服务程序要从 wq 中把它取出来唤醒。condition 这可以是一个变量，也可以是任何表达式。表示“一直等待，直到 condition 为真”。 函数 说明 wait_event_interruptible(wq, condition) 休眠，直到 condition 为真； 休眠期间是可被打断的，可以被信号打断 wait_event(wq, condition) 休眠，直到 condition 为真； 退出的唯一条件是 condition 为真，信号也不好使 wait_event_interruptible_timeout(wq, condition, timeout) 休眠，直到 condition 为真或超时； 休眠期间是可被打断的，可以被信号打断 wait_event_timeout(wq, condition, timeout) 休眠，直到 condition 为真； 退出的唯一条件是 condition 为真，信号也不好使 唤醒函数 参考内核源码：include\linux\wait.h。 函数 说明 wake_up_interruptible(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程，只唤醒其中的一个线程 wake_up_interruptible_nr(x, nr) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程，只唤醒其中的 nr 个线程 wake_up_interruptible_all(x) 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程，唤醒其中的所有线程 wake_up(x) 唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或“TASK_UNINTERRUPTIBLE”的线程，只唤醒其中的一个线程 wake_up_nr(x, nr) 唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或“TASK_UNINTERRUPTIBLE”的线程，只唤醒其中 nr 个线程 wake_up_all(x) 唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或“TASK_UNINTERRUPTIBLE”的线程，唤醒其中的所有线程 19.1.3 驱动框架 初始化 wq 队列 在驱动的 read 函数中，调用 wait_event_interruptible： 它本身会判断 event 是否为 FALSE，如果为 FASLE 表示无数据，则休眠。 在中断服务程序里： 设置 event 为 TRUE，并调用 wake_up_interruptible 唤醒线程。 19.1.4 编程 static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait); static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { int err; wait_event_interruptible(gpio_key_wait, g_key); err = copy_to_user(buf, &g_key, 4); g_key = 0; return 4; } static irqreturn_t gpio_key_isr(int irq, void *dev_id) { struct gpio_key *gpio_key = dev_id; int val; val = gpiod_get_value(gpio_key->gpiod); printk("key %d %d\n", gpio_key->gpio, val); g_key = (gpio_key->gpio << 8) | val; wake_up_interruptible(&gpio_key_wait); return IRQ_HANDLED; } 19.1.6 使用环形缓冲区改进驱动程序 /* 环形缓冲区 */ #define BUF_LEN 128 static int g_keys[BUF_LEN]; static int r, w; #define NEXT_POS(x) ((x+1) % BUF_LEN) static int is_key_buf_empty(void) { return (r == w); } static int is_key_buf_full(void) { return (r == NEXT_POS(w)); } static void put_key(int key) { if (!is_key_buf_full()) { g_keys[w] = key; w = NEXT_POS(w); } } static int get_key(void) { int key = 0; if (!is_key_buf_empty()) { key = g_keys[r]; r = NEXT_POS(r); } return key; } wait_event_interruptible(gpio_key_wait, !is_key_buf_empty()); key = get_key(); put_key(key); 19.2 POLL 机制 drv_poll 要把线程挂入队列 wq，但是并不是在 drv_poll 中进入休眠，而是在调用 drv_poll 之后休眠 19.2.3 驱动编程 把当前线程挂入队列 wq：poll_wait 把线程挂入队列，如果线程已经在队列里了，它就不会再次挂入。 返回设备状态：drv_poll 要返回自己的当前状态：(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。 POLLRDNORM 等同于 POLLIN, POLLWRNORM 等同于 POLLOUT static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); poll_wait(fp, &gpio_key_wait, wait); return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM; } 19.2.7 POLL 机制的内核代码详解 对于系统调用 poll 或 select，它们对应的内核函数都是 sys_poll。分析sys_poll，即可理解 poll 机制。 都在 fs/select.c 文件中。 sys_poll -> do_sys_poll -> do_poll 19.3 异步通知 Linux 系统中也有很多信号，在 Linux 内核源文件 include\uapi\asm-generic\signal.h 中，有很多信号的宏定义： #define SIGHUP 1 #define SIGINT 2 #define SIGQUIT 3 #define SIGILL 4 #define SIGTRAP 5 #define SIGABRT 6 #define SIGIOT 6 #define SIGBUS 7 #define SIGFPE 8 #define SIGKILL 9 #define SIGUSR1 10 #define SIGSEGV 11 #define SIGUSR2 12 #define SIGPIPE 13 #define SIGALRM 14 #define SIGTERM 15 #define SIGSTKFLT 16 #define SIGCHLD 17 #define SIGCONT 18 #define SIGSTOP 19 #define SIGTSTP 20 #define SIGTTIN 21 #define SIGTTOU 22 #define SIGURG 23 #define SIGXCPU 24 #define SIGXFSZ 25 #define SIGVTALRM 26 #define SIGPROF 27 #define SIGWINCH 28 #define SIGIO 29 #define SIGPOLL SIGIO ... 设置 Flag 里面的 FASYNC 位为 1：当 FASYNC 位发生变化时，会导致驱动程序的 fasync 被调用； 19.3.3 驱动编程 struct fasync_struct *button_fasync; static int gpio_key_drv_fasync(int fd, struct file *file, int on) { if (fasync_helper(fd, file, on, &button_fasync) >= 0) return 0; else return -EIO; } static irqreturn_t gpio_key_isr(int irq, void *dev_id) { struct gpio_key *gpio_key = dev_id; int val; int key; val = gpiod_get_value(gpio_key->gpiod); printk("key %d %d\n", gpio_key->gpio, val); key = (gpio_key->gpio << 8) | val; put_key(key); wake_up_interruptible(&gpio_key_wait); kill_fasync(&button_fasync, SIGIO, POLL_IN); return IRQ_HANDLED; } fasync_helper 函 数 会 分 配 、 构 造 一 个 fasync_struct 结 构 体 button_async： 驱动文件的 flag 被设置为 FAYNC 时：button_async->fa_file = filp; // filp 表示驱动程序文件，里面含有之前设置的 PID 驱动文件被设置为非 FASYNC 时： button_async->fa_file = NULL; 19.4 阻塞与非阻塞 O_NONBLOCK 19.4.1 应用编程 int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式 */ int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式 */ int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式 */ fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式 */ 19.4.2 驱动编程 // drv_read if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK)) return -EAGAIN; 19.5 定时器 参 考 内 核 源 码 include\linux\timer.h setup_timer(timer, fn, data)：设置定时器，主要是初始化 timer_list 结构体 void add_timer(struct timer_list *timer)：向内核添加定时器。timer->expires 表示超时时间。 当 超 时 时 间 到 达 ， 内 核 就 会 调 用 这 个 函 数 ：timer->function(timer->data)。 int mod_timer(struct timer_list *timer, unsigned long expires): 修改定时器的超时时间， int del_timer(struct timer_list *timer)：删除定时器。 19.5.2 定时器时间单位 .config 中有配置 CONFIG_HZ=100, 核每秒中会发生 100 次系统滴答中断(tick). 在 add_timer 之前，直接修改 timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时，也就是 xxx*10ms timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ，2*HZ 就相当于 2 秒 在 add_timer 之后，使用 mod_timer 修改： mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时，也就是 xxx*10ms mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ，2*HZ 就相当于 2 秒 19.5.5 深入研究：定时器的内部机制 对于 TIMER_SOFTIRQ 软中断，初始化代码如下： void __init init_timers(void) { init_timer_cpus(); init_timer_stats(); open_softirq(TIMER_SOFTIRQ, run_timer_softirq); } 当发生硬件中断时，硬件中断处理完后，内核会调用软件中断的处理函数。对于 TIMER_SOFTIRQ，会调用 run_timer_softirq，它的函数如下： run_timer_softirq __run_timers(base); while (time_after_eq(jiffies, base->clk)) { …… expire_timers(base, heads + levels); fn = timer->function; data = timer->data; call_timer_fn(timer, fn, data); fn(data); 19.5.6 深入研究：找到系统滴答 # cat /proc/interrupts 找到 ticker 中断名 $ grep "i.MX Timer Tick" * -nr 找到对应的文件 drivers/clocksource/timer-imx-gpt.c:319: act->name = "i.MX Timer Tick"; 19.6 中断下半部 tasklet 19.6.1 内核函数 定义 tasklet 它 在 内 核 源 码 include\linux\interrupt.h 中定义： struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; // DECLARE_TASKLET 定义的 tasklet 结构体，它是使能的； #define DECLARE_TASKLET(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } // DECLARE_TASKLET_DISABLED 定义的 tasklet 结构体，它是禁止的；使用之前要先调用 tasklet_enable 使能它。 #define DECLARE_TASKLET_DISABLED(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } // 使用函数来初始化 tasklet 结构体： extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); 使能/禁止 tasklet static inline void tasklet_enable(struct tasklet_struct *t); static inline void tasklet_disable(struct tasklet_struct *t); 调度 tasklet static inline void tasklet_schedule(struct tasklet_struct *t); kill tasklet extern void tasklet_kill(struct tasklet_struct *t); 19.6.2 tasklet 使用方法 先定义 tasklet，需要使用时调用 tasklet_schedule，驱动卸载前调用 tasklet_kill。tasklet_schedule 只是把 tasklet 放入内核队列，它的 func 函数会在软件中断的执行过程中被调用。 19.6.3 tasklet 内部机制 tasklet属于TASKLET_SOFTIRQ软件中断，入口函数为 tasklet_action，这在内核 kernel\softirq.c 中设置. 当驱动程序调用 tasklet_schedule 时，会设置 tasklet 的 state 为TASKLET_STATE_SCHED，并把它放入某个链表 当发生硬件中断时，内核处理完硬件中断后，会处理软件中断。对于TASKLET_SOFTIRQ 软件中断，会调用 tasklet_action 函数。 如果 tasklet 的函数尚未执行，多次调用 tasklet_schedule 也是无效的，只会放入队列一次。 19.7 工作队列 19.7.1 内核函数 定义 work #define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f) // #define DECLARE_DELAYED_WORK(n, f) \ struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0) 使用 work：schedule_work 调用 schedule_work 时，就会把 work_struct 结构体放入队列中，并唤醒对应的内核线程。内核线程就会从队列里把 work_struct 结构体取出来，执行里面的函数。 19.7.3 内部机制 代码在 kernel\workqueue.c 中 init_workqueues keventd_wq = create_workqueue("events"); __create_workqueue((name), 0, 0) for_each_possible_cpu(cpu) { err = create_workqueue_thread(cwq, cpu); p = kthread_create(worker_thread, cwq, fmt, wq->name, cpu); 19.8 中断的线程化处理 19.8.1 内核机制 request_threaded_irq 函数，肯定会创建一个内核线程。源码在内核文件 kernel\irq\manage.c 中 19.8.2 编程、上机 调用 request_threaded_irq 函数注册中断，调用 free_irq 卸载中断。 19.9 mmap 让程序可以直接读写驱动程序中的 buffer，这可以通过mmap 实现(memory map)，把内核的 buffer 映射到用户态，让 APP 在用户态直接读写。
100ask 韦东山 第四篇 嵌入式 Linux 应用开发 视频零散知识点
https://www.bilibili.com/video/BV1kk4y117Tu 4【第四章】文件I/O 4_6_综合实验_处理表格 hexdump -C main.c 可以用来查看二进制数据，并且在右侧显示相应的文本对照。 4_7_文件IO系统调用内部机制 /proc/pid/fd 对应进程打开的 fd，默认有 0,1,2 三个，对应 stdin, stdout, stderr. 内部调用 fs/open.c SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) -> do_sys_open(AT_FDCWD, filename, flags, mode) do_sys_open(AT_FDCWD, filename, flags, mode) -> fd = get_unused_fd_flags(flags) 获取未使用的文件描述符, struct file *f = do_filp_open(dfd, tmp, &op) 打开文件结构体 , fd_install(fd, f) 把文件结构体放入文件描述符数组中。 fs/namei.c struct file *do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op) fs/file.c int get_unused_fd_flags(unsigned flags) -> __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags) -> fdt = files_fdtable(files) -> fd = find_next_fd(fdt, fd), files->next_fd = fd + 1 fs/file.c void fd_install(unsigned int fd, struct file *file) -> __fd_install(current->files, fd, file) -> fdt = rcu_dereference_sched(files->fdt), rcu_assign_pointer(fdt->fd[fd], file) current 来源于 include/asm-generic #define current get_current() -> #define get_current() (current_thread_info()->task) -> thread_info.h 里面的 struct thread_info -> sched.h struct task_struct *task -> fdtable.h struct files_struct *files -> struct fdtable fdtab 4_8_dup函数的使用 int dup(int old); , 返回一个指向 old 的新的最小未使用 fd. int dup2(int old,int new); , 先检查 new 有没有被打开，如果打开，就先关闭。 然后让 new 指向 old. 第7章 输入系统应用编程 7.2 现场编程获取输入设备信息 drivers\input\evdev.c, static long evdev_do_ioctl(struct file *file, unsigned int cmd, void __user *p, int compat_mode) 内核中 ioctl 对应的函数 evdev_do_ioctl 中 switch 判断分支 EVIOCGVERSION 在 include\uapi\linux\input.h 中 #define EVIOCGVERSION _IOR('E', 0x01, int) /* get driver version */ #define EVIOCGID _IOR('E', 0x02, struct input_id) /* get device ID */ #define EVIOCGREP _IOR('E', 0x03, unsigned int[2]) /* get repeat settings */ #define EVIOCSREP _IOW('E', 0x03, unsigned int[2]) /* set repeat settings */ // #define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) /* get event bits */ 获取设备 id 代码 struct input_id id; err = ioctl(fd, EVIOCGID, &id); if (err == 0) { printf("bustype = 0x%x\n", id.bustype ); printf("vendor = 0x%x\n", id.vendor ); printf("product = 0x%x\n", id.product ); printf("version = 0x%x\n", id.version ); } 获取设备 evbits 代码, EVIOCGBIT(ev,len) 中 ev 为 0 表示获取 evbit, 为 1 表示获取 keybit，按照 include\linux\input.h 里面的 struct input_dev 中的偏移来算。 unsigned int evbit[2]; len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); if (len > 0 && len <= sizeof(evbit)) { printf("support ev type: "); for (i = 0; i < len; i++) { byte = ((unsigned char *)evbit)[i]; for (bit = 0; bit < 8; bit++) { if (byte & (1<<bit)) { printf("%s ", ev_names[i*8 + bit]); } } } printf("\n"); } 7_3 查询 休眠唤醒 读取输入数据 read 对应的驱动中的函数在 drivers\input\evdev.c static ssize_t evdev_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos), 通过 input_event_to_user(buffer + read, &event) 给用户空间传数据，struct input_event event. 应用中读取数据的代码: while (1) { len = read(fd, &event, sizeof(event)); if (len == sizeof(event)) { printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); } else { printf("read err %d\n", len); } } 7_6 电阻屏和电容屏 上报数据中的 ABS_MT_TOUCH_MAJOR 和 ABS_MT_WIDTH_MAJOR 指的是接触区域的椭圆尺寸，正常没啥用。 7_7 tslib框架分析 tslib，暂时不太需要，以后需要的时候，再回头看。 第8章 网络通信 8_2 tcp 编程 tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */ , 自己给出来的数据，需要进行字节序转换。 iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr)); 把方便的 struct sockaddr_in 转为不方便的 struct sockaddr iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen); 服务端接收数据 if (-1 != iSocketClient) { iClientNum++; printf("Get connect from client %d : %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr)); if (!fork()) { /* 子进程的源码 */ while (1) { /* 接收客户端发来的数据并显示出来 */ iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0); if (iRecvLen <= 0) { close(iSocketClient); return -1; } else { ucRecvBuf[iRecvLen] = '\0'; printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf); } } } } signal(SIGCHLD,SIG_IGN); 显式的忽略子进程退出信号，防止 fork 子进程退出后变为僵尸进程。 当然也可以根据需要设置一个信号处理函数。 iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr)); 客户端发送数据 if (-1 == iRet) { printf("connect error!\n"); return -1; } while (1) { if (fgets(ucSendBuf, 999, stdin)) { iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0); if (iSendLen <= 0) { close(iSocketClient); return -1; } } } 8_3 udp 编程 服务端接收数据 while (1) { iAddrLen = sizeof(struct sockaddr); iRecvLen = recvfrom(iSocketServer, ucRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen); if (iRecvLen > 0) { ucRecvBuf[iRecvLen] = '\0'; printf("Get Msg From %s : %s\n", inet_ntoa(tSocketClientAddr.sin_addr), ucRecvBuf); } } 客户端发送数据 while (1) { if (fgets(ucSendBuf, 999, stdin)) { iAddrLen = sizeof(struct sockaddr); iSendLen = sendto(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0, (const struct sockaddr *)&tSocketServerAddr, iAddrLen); if (iSendLen <= 0) { close(iSocketClient); return -1; } } } 串口 10_3 tty 体系设备节点 tty 终端，ubuntu 直接切换的就是 tty, tty0 特指前台终端，如果说当前切换到了 tty3，那么 tty0 也是指向 tty3. 可以使用 echo hello > /dev/tty3 来向 tty3 发消息，也可以用 echo hello > /dev/tty0 向前台发消息。 while [ 1 ]; do echo msg_to_front > /dev/tty0; sleep 3; done 来间隔3秒向前台发信息。就算切换 tty，也可以自动在当前前台显示出来。 /dev/tty 不含数字的 tty，表示当前 shell 所对应的终端，如果当前是 tty3, 那么 访问 /dev/tty 就是访问 /dev/tty3. 串口对应的 tty，一般是 ttyS0 或者 ttymxc0 等等。 console 表示控制台，比 tty 权限大。 正常情况下，一般是选择某个 tty 作为 console. 内核启动时，可以用 cmdline 来指定，比如 console=tty1, 如果有多个值的时候，以最后一个为准。 **注意: console=tty0 和 console=tty 都表示使用前台作为 console. 可以通过 cat /proc/cmdline 来获取 cmdline 的参数设置。 如果 cmdline 中没有指定 console，那么默认就是第一个 tty，根据驱动安装的先后，可能是第一个串口，也有可能是第一个虚拟终端。 在 reboot 之后，可以按住 esc 进入系统选择界面，选择 advance 选项，把 linux 这一行中的 quiet 去掉，然后增加 console=tty0 , 然后退出选择第一个进入系统。 系统启动过程中 console 是等于前台，等到系统启动完成，才会按照指定的的 console 来对应设备。 console 也是可以用 echo hello > /dev/console 来打印信息。 10_4 tty 驱动程序框架 The TTY demystified, 解密TTY 行编辑。大多数用户都会在输入时犯错，所以退格键会很有用。这当然可以由应用程序本身来实现，但是根据UNIX设计“哲学”，应用程序应尽可能保持简单。为了方便起见，操作系统提供了一个编辑缓冲区和一些基本的编辑命令（退格，清除单个单词，清除行，重新打印），这些命令在行规范（line discipline）内默认启用。高级应用程序可以通过将行规范设置为原始模式（raw mode）而不是默认的成熟或准则模式（cooked and canonical）来禁用这些功能。大多数交互程序（编辑器，邮件客户端，shell，及所有依赖curses或readline的程序）均以原始模式运行，并自行处理所有的行编辑命令。行规范还包含字符回显和回车换行（译者注：\r\n 和 \n）间自动转换的选项。如果你喜欢，可以把它看作是一个原始的内核级sed(1)
100ask 韦东山 第四篇 嵌入式 Linux 应用开发 学习笔记
2 编译器的使用 2.1 gcc 编译过程 使用 gcc -o hello hello.c -v 之后，会打印出详细的编译过程，去除不重要的信息后，最后剩余的就是三步，分别是：编译为 .s 汇编文件，汇编为 .o 机器码，链接为最终的可执行文件。具体三条信息如下： /usr/lib/gcc/x86_64-linux-gnu/7/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccX6eBn7.s as -v --64 -o /tmp/cclwBtJD.o /tmp/ccX6eBn7.s /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=/tmp/cc7xr459.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. /tmp/cclwBtJD.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o 进一步去除编译选项后，命令主题如下: # 编译 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccX6eBn7.s # 汇编 as -o /tmp/cclwBtJD.o /tmp/ccX6eBn7.s # 链接 /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -o hello /tmp/cclwBtJD.o 手动分步编译: gcc -E -o hello.i hello.c gcc -S -o hello.s hello.i gcc -c -o hello.o hello.s gcc -o hello hello.o 2.1.2 编译多个文件 一起编译 gcc -o test main.c sub.c 分开编译，统一链接 gcc -c -o main.o main.c gcc -c -o sub.o sub.c gcc -o test main.o sub.o 2.1.3 制作、使用动态库 gcc -shared -o libsub.so sub.o sub2.o sub3.o(可以使用多个.o 生成动态库) gcc -o test main.o -lsub -L /libsub.so/所在目录/ 运行 # 放在某个目录比如/a export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/a ./test 2.1.4 制作、使用静态库 ar crs libsub.a sub.o sub2.o sub3.o(可以使用多个.o 生成静态库) gcc -o test main.o libsub.a (如果.a 不在当前目录下，需要指定它的绝对或相对路径) 如果执行 arm-buildroot-linux-gnueabihf-gcc -c -o sub.o sub.c 交叉编译需要在最后面加上-fPIC 参数 2.1.5 很有用的选项 gcc -E main.c // 查看预处理结果，比如头文件是哪个 gcc -E -dM main.c > 1.txt // 把所有的宏展开，存在 1.txt 里 gcc -Wp,-MD,abc.dep -c -o main.o main.c // 生成依赖文件 abc.dep，后面 Makefile 会用 echo 'main(){}'| gcc -E -v - // 它会列出头文件目录、库目录(LIBRARY_PATH) 2.4 警告选项(Warning Option)-Wall 开启全部警告 gcc -Wall -c main.c 2.5 调试选项(Debugging Option)-g 除了基本的 -g 以外，还有 -gstabs+，-gstabs，-gxcoff+，-gxcoff，-gdwarf+ 或 -gdwarf 2.6 优化选项(Optimization Option) O0: 不优化, 只有声明了 register 的变量才分配使用寄存器。 O1: -fthread-jumps和-fdefer-pop选项将被打开。在有 delay slot 的机器上，-fdelayed-branch选项将被打开。在即使没有帧指针 (frame pointer)也支持调试的机器上，-fomit-frame-pointer选项将被打开。某些机器上还可能会打开其他选项。 O2: 除了涉及空间和速度交换的优化选项，执行几乎所有的优化工作。例如不进行循环展开(loop unrolling)和函数内嵌(inlining)。 O3: 除了打开-O2 所做的一切，它还打开了-finline-functions选项。 2.7 链接器选项(Linker Option) -llibrary ，库文件的真正名字是liblibrary.a, 指定-l选项和指定文件名的唯一区别是，-l选项用 lib和.a把 library 包裹起来，而且搜索一些目录。 -nostartfiles 不链接系统标准启动文件，而标准库文件仍然正常使用： -nostdlib 不链接系统标准启动文件和标准库文件，只把指定的文件传递给链接器。这个选项常用于编译内核、bootloader 等程序，它们不需要启动文件、标准库文件。 -static 阻止链接共享库 -shared 生成一个共享 OBJ 文件，它可以和其他 OBJ 文件链接产生可执行文件。当不想以源代码发布程序时，可以使用-shared 选项生成库文件. -Xlinker option 。如果需要传递携带参数的选项，必须使用两次-Xlinker，一次传递选项，另一次传递其参数。例如，如果传递-assert definitions，要成-Xlinker -assert -Xlinker definitions， 2.8 目录选项(Directory Option) -Bprefix 这个选项指出在何处寻找可执行文件，库文件，以及编译器自己的数据文件。编译器驱动程序需要使用某些工具，比如：cpp'，cc1' (或 C++的cc1plus')，as'和`ld'。 第3章 Makefile 的使用 3.1 配套视频内容大纲 Makefile 规则与示例 介绍 Makefile 的 2 个函数 $(foreach var,list,text) : 对 list 中的每一个元素，取出来赋给 var，然后把 var 改为 text 所描述的形式。 objs := a.o b.o dep_files := $(foreach f, $(objs), .$(f).d) // 最终 dep_files := .a.o.d .b.o.d $(wildcard pattern) : pattern 所列出的文件是否存在，把存在的文件都列出来。 src_files := $( wildcard *.c) // 最终 src_files 中列出了当前目录下的所有.c 文件 第4章 文件 IO 4.2 怎么访问文件？ 4.2.2 不是通用的函数：ioctl/mmap void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); 将一个普通文件映射到内存中，通常在需要对文件进行频繁读写时使用，这样用内存读写取代I/O读写，以获得较高的性能. buf = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd_old, 0); if (buf == MAP_FAILED) { printf("can not mmap file %s\n", argv[1]); return -1; } if (write(fd_new, buf, stat.st_size) != stat.st_size) { printf("can not write %s\n", argv[2]); return -1; } ** mmap 详细介绍，可以参考 认真分析mmap：是什么 为什么 怎么用 4.3 怎么知道这些函数的用法？ man 的几种分类 Executable programs or shell commands // 命令 System calls (functions provided by the kernel) // 系统调用，比如 man 2 open Library calls (functions within program libraries) // 函数库调用 Special files (usually found in /dev) // 特殊文件, 比如 man 4 tty File formats and conventions eg /etc/passwd // 文件格式和约定, 比如 man 5 passwd Games // 游戏 Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7) / /杂项 System administration commands (usually only for root) // 系统管理命令 Kernel routines [Non standard] // 内核例程 常用快捷键 f 往前翻一页 b 往后翻一页 /patten 往前搜 ?patten 往后搜 第5章 Framebuffer 应用编程 5.1 LCD 操作原理 32BPP，一般只设置其中的低 24 位，高 8 位表示透明度，一般的 LCD 都不支持 24BPP，硬件上为了方便处理，在 Framebuffer 中也是用 32 位来表示，效果跟 32BPP 是一样的 16BPP，常用的是 RGB565；很少的场合会用到 RGB555，这可以通过 ioctl 读取驱动程序中的 RGB 位偏移来确定使用哪一种格式 5.3 Framebuffer 程序分析 5.3.1 打开设备 fd_fb = open("/dev/fb0", O_RDWR); if (fd_fb < 0) { printf("can't open /dev/fb0\n"); return -1; } 5.3.2 获取 LCD 参数 LCD 驱动程序给 APP 提供 2 类参数：可变的参数 fb_var_screeninfo、固定的参数 fb_fix_screeninfo。编写应用程序时主要关心可变参数，它的结构体定义如下(#include <linux/fb.h>) static struct fb_var_screeninfo var; // 获取 fb_var_screeninfo if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var)) { printf("can't get var\n"); return -1; } 5.3.3 映射 Framebuffer screen_size = var.xres * var.yres * var.bits_per_pixel / 8; fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); if (fb_base == (unsigned char *)-1) { printf("can't mmap\n"); return -1; } 第6章 文字显示 6.1 字符的编码方式 6.1.2 UNICODE 编码实现 utf16: 分为 le 和 be 两种，都是占用两个字节 utf8: 可变字节，ascii 还是一个字节，中文之类的会用三个字节，第一个字节开头 1110 表示当前字符需要占用3个字节，第二个和第三个字节开头都是 10，表示使用了1个字节。 第一个字节的低4位和第二第三字节的低6位，一起组成16位的字符数据。 6.3 中文字符的点阵显示 6.3.1 指定编码格式 -finput-charset, 编译程序时，指定源文件字符编码参数 -finput-charset=GB2312, -finput-charset=UTF-8 -fexec-charset, 编译程序时，指定可执行文件编码参数 -fexec-charset=GB2312, -fexec-charset=UTF-8. 如果 -finput-charset 和 -fexec-charset 不一致，编译器会自动进行格式转换。 一般情况下，指定 -fexec-charset即可。但是如果你源文件使用了 gb2312 保存，那就需要指定 -finput-charset 6.3.4 汉字点阵显示实验 字库频繁使用，使用 mmap 每次只需要 1次复制即可。 4787 fd_hzk16 = open("HZK16", O_RDONLY); 4788 if (fd_hzk16 < 0) 4789 { 4790 printf("can't open HZK16\n"); 4791 return -1; 4792 } 4793 if(fstat(fd_hzk16, &hzk_stat)) 4794 { 4795 printf("can't get fstat\n"); 4796 return -1; 4797 } 4798 hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARE D, fd_hzk16, 0); 4799 if (hzkmem == (unsigned char *)-1) 4800 { 4801 printf("can't mmap for hzk16\n"); 4802 return -1; 4803 } 6.4 交叉编译程序：以 freetype 为例 6.4.3 交叉编译程序的万能命令 ./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp ， 这个命令里面的 --host 用来指定当前的交叉编译工具链， --prefix 用来指定编译安装目录。 这个命令执行完了之后，就可以使用 make 来编译项目了，再使用 make install 来安装到指定目录。 cp -rfd， d 选项用于需要复制的文件中有链接文件时来使用，能够继续保证正确的链接。 查看当前编译工具链用到的目录，可以使用 echo 'main(){}'| arm-buildroot-linux-gnueabihf-gcc -E -v - 这个命令，注意观察输出中的 #include "..." search starts here:, #include <...> search starts here:, COMPILER_PATH, LIBRARY_PATH 等等。 运行的时候缺少库文件，可以通过 export LD_LIBRARY_PATH= 增加相关的库文件目录。 6.5 使用 freetype 显示单个文字 6.5.3 在 LCD 上显示一个矢量字体 wchar_t 中保存的是 UNICODE, 比较方便字符显示。 普通的 char 中保存的是 gb2312 或者 utf-8. wchar_t *chinese_str = L"中gif""; 可以这样使用 wchar_t wcslen(chinese_str) 可以用来获取 unicode 字符串长度。 第7章 输入系统应用编程 7.2 输入系统框架及调试 7.2.1 框架概述 数据流: APP 发起读操作，若无数据则休眠 用户操作设备，硬件上产生中断； 输入系统驱动层对应的驱动程序处理中断： 读取到数据，转换为标准的输入事件，向核心层汇报。 所谓输入事件就是一个“struct input_event”结构体。 核心层可以决定把输入事件转发给上面哪个 handler 来处理： 从 handler 的名字来看，它就是用来处输入操作的。有多种 handler，比如：evdev_handler、kbd_handler、joydev_handler 等等。 最常用的是 evdev_handler：它只是把 input_event 结构体保存在内核buffer 等，APP 来读取时就原原本本地返回。它支持多个 APP 同时访问输入设备，每个 APP 都可以获得同一份输入事件。 当 APP 正在等待数据时，evdev_handler 会把它唤醒，这样 APP 就可以返 回数据。 APP 对输入事件的处理： APP 获 得 数 据 的 方 法 有 2 种 ： 直 接 访 问 设 备 节 点 ( 比 如 /dev/input/event0,1,2,...)，或者通过 tslib、libinput 这类库来间接访问设备节点。这些库简化了对数据的处理。 7.2.2 编写 APP 需要掌握的知识 内核中怎么表示一个输入设备？ include/linux/input.h struct input_dev { ... // 支持事件类型， key/rel/abs unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 支持按键类型 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 支持相对位移, 鼠标等 unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 支持绝对位移，触摸屏等 unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; ... } struct input_event 在 include/uapi/linux/input.h 中 struct input_event { // include/uapi/linux/time.h struct timeval time; // include/uapi/linux/input-event-codes.h , EV_* __u16 type; // include/uapi/linux/input-event-codes.h , KEY_*, REL_*, ABS_* __u16 code; // value 可以是 0(表示按键被按下)、1(表示按键被松开)、2(表示长按)； 或者坐标值等等 __s32 value; }; 数据全部上报后，驱动会再发一个“同步事件”，表示上报结束。同步事件也是一个 input_event 结构体，它的 type、code、value 三项都是 0。 查看输入节点 ls -l /dev/input/* 看看有哪些 event 获取输入节点的详细信息 cat /proc/bus/input/devices, 可以指导输入设备具体对应 input 下面的 event 序号。 节点详细信息中，I 表示 struct input_id, N 表示 name, P 表示物理路径，S 表示 sys 路径，U unqiue code, H 设备关联的句柄列表，B 表示位图 B 位图，按照二进制对应的 1 所在的位置，来确定含义。 B: EV=b，为 1011 ， 表示支持 EV_SYN(0)、EV_KEY(1)、EV_ABS(3). B: ABS=2658000 3, 表示支持 0、1、47、48、50、53、54 功能。 hexdump /dev/input/event1 使用 hexdump 来获取 event1 即触摸屏的输入数据。 7.3 不使用库的应用程序示例 7.3.3 获取设备信息 int ioctl(int fd, unsigned long request, ...); 通过 ioctl 获取设备信息, 有些驱动程序对 request 的格式有要求. 可以用来获取设备版本号，id，支持的类型等等相关信息。 // include\uapi\asm-generic\ioctl.h #define _IOC(dir,type,nr,size) \ (((dir) << _IOC_DIRSHIFT) | \ // bit 29 , _IOC_READ, _IOC_WRITE ((type) << _IOC_TYPESHIFT) | \ // bit 8, 具体的驱动程序决定 ((nr) << _IOC_NRSHIFT) | \ // bit 0, 具体的驱动程序决定 ((size) << _IOC_SIZESHIFT)) // bit 16, ioctl 能传输数据的最大字节数 7.3.4 查询方式 open 时候，用 O_NONBLOCK，然后用 read 来读就可以。如果驱动程序中有数据，那么 APP 的 read函数会返回数据，否则也会立刻返回错误。 7.3.5 休眠-唤醒方式 open 时候，不用 O_NONBLOCK，read 没数据，就休眠。 7.3.6 POLL/SELECT 方式 用 poll/select 来监控输入文件，有数据，再用 read 来读取。 struct pollfd fds[1]; int timeout_ms = 5000; int ret; fds[0].fd = fd; fds[0].events = POLLIN; ret = poll(fds, 1, timeout_ms); if ((ret == 1) && (fds[0].revents & POLLIN)) { read(fd, &val, 4); printf("get button : 0x%x\n", val); } 7.3.7 异步通知方式 信号在 include\uapi\asm-generic\signal.h 中 #define SIGHUP 1 #define SIGINT 2 #define SIGQUIT 3 #define SIGILL 4 #define SIGTRAP 5 #define SIGABRT 6 #define SIGIOT 6 #define SIGBUS 7 #define SIGFPE 8 #define SIGKILL 9 ... #define SIGIO 29 #define SIGPOLL SIGIO ... 设置信号的几个步骤 // 编写信号处理函数： static void sig_func(int sig) { int val; read(fd, &val, 4); printf("get button : 0x%x\n", val); } // 注册信号处理函数： signal(SIGIO, sig_func); // 打开驱动： fd = open(argv[1], O_RDWR); // 把进程 ID 告诉驱动： fcntl(fd, F_SETOWN, getpid()); // 使能驱动的 FASYNC 功能： flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC); 7.4 电阻屏和电容屏 7.4.1 电阻屏 按下 EV_KEY BTN_TOUCH 1 /* 按下 */ EV_ABS ABS_PRESSURE 1 /* 压力值，可以上报，也可以不报，可以是其他压力值 */ EV_ABS ABS_X x_value /* X 坐标 */ EV_ABS ABS_Y y_value /* Y 坐标 */ EV_SYNC 0 0 /* 同步事件 */ 松开 EV_KEY BTN_TOUCH 0 /* 松开 */ EV_ABS ABS_PRESSURE 0 /* 压力值，可以上报，也可以不报 */ EV_SYNC 0 0 /* 同步事件 */ 7.4.2 电容屏 官方文档，Linux 内 核 Documentation\input\multi-touch-protocol.rst 多点触摸数据 EV_ABS ABS_MT_SLOT 0 // 这表示“我要上报一个触点信息了”，用来分隔触点信息 EV_ABS ABS_MT_TRACKING_ID 45 // 这个触点的 ID 是 45 EV_ABS ABS_MT_POSITION_X x[0] // 触点 X 坐标 EV_ABS ABS_MT_POSITION_Y y[0] // 触点 Y 坐标 EV_ABS ABS_MT_SLOT 1 // 这表示“我要上报一个触点信息了”，用来分隔触点信息 EV_ABS ABS_MT_TRACKING_ID 46 // 这个触点的 ID 是 46 EV_ABS ABS_MT_POSITION_X x[1] // 触点 X 坐标 EV_ABS ABS_MT_POSITION_Y y[1] // 触点 Y 坐标 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕 触点移动 EV_ABS ABS_MT_SLOT 0 // 这表示“我要上报一个触点信息了”，之前上报过 ID，就不用再上报 ID 了 EV_ABS ABS_MT_POSITION_X x[0] // 触点 X 坐标 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕 触点松开 EV_ABS ABS_MT_SLOT 1 // 这表示“我要上报一个触点信息了”，在前面设置过 slot 1 的 ID 为 46 EV_ABS ABS_MT_TRACKING_ID -1 // ID 为-1，表示 slot 1 被松开，即 ID 为 46 的触点被松开 EV_SYNC SYN_REPORT // 全部数据上报完毕 实际数据单点，可以看到不仅仅按照电容屏的格式上报数据，同时也按照电阻屏的数据上报了。 实际数据多点, 为了兼容老程序，它也上报了 ABS_X、ABS_Y 数据，但是只上报第 1 个触点 的数据。 7.5 tslib 使用 tslib ，就不需要自己使用 open 之类的函数，并且可以对原始数据进行更多的处理，比如过滤跳动过大的数据 7.5.1 tslib 框架分析 文件组织, 其中含有 mt 的表示多点触摸 代码框架 tslib，暂时不太需要，以后需要的时候，再回头看。 第8章 网络通信 8.1 网络通信概述 8.1.3 两种传输方式：TCP/UDP tcp udp 8.2 网络编程主要函数介绍 8.2.1 socket 函数 int socket(int domain, int type,int protocol); domain (AF_UNIX 和 AF_INET 等) type (SOCK_STREAM,SOCK_DGRAM 等)。 protocol，一般只要用 0 来代替就可以了。 8.2.2 bind 函数 int bind(int sockfd, struct sockaddr *my_addr, int addrlen); struct sockaddr{ unisgned short as_family; char sa_data[14]; }; // 由 于系 统 的 兼 容 性 , 我 们 一 般 使 用 另 外 一 个 结 构 (struct sockaddr_in) 来代替。 struct sockaddr_in{ unsigned short sin_family; // 一般为 AF_INET unsigned short sin_port; struct in_addr sin_addr; // INADDR_ANY 表示可以和任何的主机通信。 unsigned char sin_zero[8]; } 8.2.3 listen 函数 int listen(int sockfd,int backlog); backlog 设置请求排队的最大长度。当有多个客户端程序和服务端相连时，使用这个表示可以介绍的排队长度。 8.2.4 accept 函数 int accept(int sockfd, struct sockaddr *addr,int *addrlen); addr，addrlen 是用来给客户端的程序填写的,服务器端只要传递指针就可以了， bind,listen 和 accept 是服务器端用的函数。 8.2.5 connect 函数 int connect(int sockfd, struct sockaddr * serv_addr,int addrlen); serv_addr 储存了服务器端的连接信息，其中 sin_add 是服务端的地址。 addrlen 是 serv_addr 的长度 8.2.6 send 函数 ssize_t send(int sockfd, const void *buf, size_t len, int flags); flags 一般置 0。 8.2.7 recv 函数 ssize_t recv(int sockfd, void *buf, size_t len, int flags); flags 一般置 0。 8.2.8 recvfrom 函数 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 8.2.9 sendto 函数 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 第9章 多线程编程 9.1 线程的使用 9.1.3 线程的标识 pthread_t pthread_t pthread_self(void);, 线程号，其仅仅在其所属的进程上下文中才有意义 因采用 POSIX 线程接口，故在要编译的时候包含 pthread 库 -lpthread 9.1.4 线程的创建 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); pthread_t 指针，用来保存新建线程的线程号 线程的属性，一般传入 NULL 表示默认属性； 函数指针，就是线程执行的函数。这个函数返回值为 void， 形参为 void 第四个参数则表示为向线程处理函数传入的参数，若不传入，可用 NULL 填充 9.1.6 线程的退出与回收 线程主动退出, void pthread_exit(void *retval); 线程被动退出, int pthread_cancel(pthread_t thread); 线程资源回收(阻塞方式), int pthread_join(pthread_t thread, void **retval); 线程资源回收(非阻塞方式), int pthread_tryjoin_np(pthread_t thread, void **retval); 9.2 线程的控制 9.2.2 互斥锁 API 简述 初始化互斥量 , int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr); 或者用宏来快速初始化, pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER; 互斥量加锁/解锁, int pthread_mutex_lock(pthread_mutex_t *mutex);, int pthread_mutex_unlock(pthread_mutex_t *mutex); 互斥量加锁(非阻塞方式), int pthread_mutex_trylock(pthread_mutex_t *mutex); 销毁互斥量, int pthread_mutex_destory(pthread_mutex_t *mutex); 9.2.3 多线程编执行顺序控制 引入了信号量的概念，解决线程执行顺序。 9.2.4 信号量 API 简述 初始化信号量, int sem_init(sem_t *sem,int pshared,unsigned int value); 信号量 P/V 操作, int sem_wait(sem_t *sem);, int sem_post(sem_t *sem); 信号量申请(非阻塞方式), int sem_trywait(sem_t *sem); 信号量销毁 , int sem_destory(sem_t *sem); 9.2.5 条件变量 参考《Unix_Linux_Windows_OpenMP 多线程编程.pdf》，作者不详。 条件变量时一种同步机制，用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息，因此条件变量时结合互斥量来使用的。 创建和销毁条件变量, // 初始化条件变量 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_at tr 通常为 NULL // 销毁条件变量 int pthread_cond_destroy(pthread_cond_t *cond); 这些函数成功时都返回 0 等待条件变量 , 要结合互斥量一起使用 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); ... pthread_mutex_lock(&g_tMutex); // 如果条件不满足则，会 unlock g_tMutex // 条件满足后被唤醒，会 lock g_tMutex pthread_cond_wait(&g_tConVar, &g_tMutex); /* 操作临界资源 */ pthread_mutex_unlock(&g_tMutex); 通知条件变量 int pthread_cond_signal(pthread_cond_t *cond);, 只会唤醒一个等待 cond 条件变量的线程 第10章 I2C 应用编程 10.1 I2C 视频介绍 10.1.2 I2C 软件框架 10.3 SMBus 协议 Linux 内核文档：Documentation\i2c\smbus-protocol.rst 10.3.1 SMBus 是 I2C 协议的一个子集 SMBus: System Management Bus，系统管理总线。SMBus 是基于 I2C 协议的，SMBus 要求更严格，SMBus 是 I2C 协议的子集。 10.3.2 SMBus 协议分析 对于 SMBus 协议，它定义了几种数据格式。文档中的 Functionality flag 是 Linux 的某个 I2C 控制器驱动所 支持的功能。比如 Functionality flag: I2C_FUNC_SMBUS_QUICK，表示需要 I2C 控制器支持 SMBus Quick Command symbols(符号) S (1 bit) : Start bit(开始位) Sr (1 bit) : 重复的开始位 P (1 bit) : Stop bit(停止位) R/W# (1 bit) : Read/Write bit. Rd equals 1, Wr equals 0.(读写位) A, N (1 bit) : Accept and reverse accept bit.(回应位) Address(7 bits): I2C 7 bit address. Note that this can be expanded as usual to get a 10 bit I2C address. (地址位，7 位地址) Command Code (8 bits): Command byte, a data byte which often selects a register on the device. (命令字节，一般用来选择芯片内部的寄存器) Data Byte (8 bits): A plain data byte. Sometimes, I write DataLow, DataHigh for 16 bit data. (数据字节，8 位；如果是 16 位数据的话，用 2 个字节来表示：DataLow、DataHigh) Count (8 bits): A data byte containing the length of a block operation. (在 block 操作总，表示数据长度) [..]: Data sent by I2C device, as opposed to data sent by the host adapter. (中括号表示 I2C 设备发送的数据，没有中括号表示 host adapter 发送的数据) 10.3.3 SMBus 和 I2C 的建议 因为很多设备都实现了 SMBus，而不是更宽泛的 I2C 协议，所以优先使用SMBus。 10.4 I2C 系统的重要结构体 Linux 驱动程序, /drivers/i2c 10.4.1 重要结构体 使用一句话概括 I2C 传输：APP 通过 I2C Controller 与 I2C Device 传输数据。 I2C BUS, include\linux\i2c.h, struct i2c_adapter I2C BUS 的传输函数, struct i2c_algorithm -> int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); 具体的数据消息, include\uapi\linux\i2c.h, struct i2c_msg I2C Device, include\linux\i2c.h, struct i2c_client -> struct i2c_adapter *adapter, unsigned short addr; i2c_msg 中的 flags 用来表示传输方向, 一个 i2c_msg 要么是读，要么是写，所以读操作，正常需要构造两个 i2c_msg 10.4.2 内核里怎么传输数据 应用通过 i2c_adapter 与 i2c_client 传输 i2c_msg 内核使用 i2c_transfer, drivers\i2c\i2c-core.c, int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) 10.5 无需编写驱动程序即可访问 I2C 设备 APP 访问硬件肯定是需要驱动程序的，对于 I2C 设备，内核提供了驱动程序drivers/i2c/i2c-dev.c，通过它可以直接使用下面的 I2C 控制器驱动程序来访问 I2C 设备。 10.5.1 体验 I2C-Tools i2cdetect：I2C 检测 // 列出当前的 I2C Adapter(或称为 I2C Bus、I2C Controller) i2cdetect -l // 打印某个 I2C Adapter 的 Functionalities, I2CBUS 为 0、1、2 等整数 i2cdetect -F I2CBUS // 看看有哪些 I2C 设备, I2CBUS 为 0、1、2 等整数 i2cdetect -y -a I2CBUS i2cget：I2C 读 // 读一个字节: I2CBUS 为 0、1、2 等整数, 表示 I2C Bus; CHIP-ADDRESS 表示设备地址 i2cget -f -y I2CBUS CHIP-ADDRESS // 读某个地址上的一个字节: // I2CBUS 为 0、1、2 等整数, 表示 I2C Bus // CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS: 芯片上寄存器地址 // MODE：有 2 个取值, b-使用`SMBus Read Byte`先发出 DATA-ADDRESS, 再读一个字节, 中间无 P 信号 // c-先 write byte, 在 read byte，中间有 P 信号 i2cget -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS MODE // 读某个地址上的 2 个字节: // I2CBUS 为 0、1、2 等整数, 表示 I2C Bus // CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS: 芯片上寄存器地址 // MODE：w-表示先发出 DATA-ADDRESS，再读 2 个字节 i2cget -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS MODE i2cset：I2C 写 // 写一个字节: I2CBUS 为 0、1、2 等整数, 表示 I2C Bus; CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS 就是要写的数据 i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS // 给 address 写 1 个字节(address, value): // I2CBUS 为 0、1、2 等整数, 表示 I2C Bus; CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS: 8 位芯片寄存器地址; // VALUE: 8 位数值 // MODE: 可以省略，也可以写为 b i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE [b] // 给 address 写 2 个字节(address, value): // I2CBUS 为 0、1、2 等整数, 表示 I2C Bus; CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS: 8 位芯片寄存器地址; // VALUE: 16 位数值 // MODE: w i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE w // SMBus Block Write：给 address 写 N 个字节的数据 // 发送的数据有：address, N, value1, value2, ..., valueN // 跟`I2C Block Write`相比, 需要发送长度 N // I2CBUS 为 0、1、2 等整数, 表示 I2C Bus; CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS: 8 位芯片寄存器地址; // VALUE1~N: N 个 8 位数值 // MODE: s i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE1 ... VALUEN s // I2C Block Write：给 address 写 N 个字节的数据 // 发送的数据有：address, value1, value2, ..., valueN // 跟`SMBus Block Write`相比, 不需要发送长度 N // I2CBUS 为 0、1、2 等整数, 表示 I2C Bus; CHIP-ADDRESS 表示设备地址 // DATA-ADDRESS: 8 位芯片寄存器地址; // VALUE1~N: N 个 8 位数值 // MODE: i i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE1 ... VALUEN i i2ctransfer：I2C 传输(不是基于 SMBus) // Example (bus 0, read 8 byte at offset 0x64 from EEPROM at 0x50): # i2ctransfer -f -y 0 w1@0x50 0x64 r8 // Example (bus 0, write 3 byte at offset 0x64 from EEPROM at 0x50): # i2ctransfer -f -y 0 w9@0x50 0x64 val1 val2 val3 // Example // first: (bus 0, write 3 byte at offset 0x64 from EEPROM at 0x50) // and then: (bus 0, read 3 byte at offset 0x64 from EEPROM at 0x50) # i2ctransfer -f -y 0 w9@0x50 0x64 val1 val2 val3 r3@0x50 # i2ctransfer -f -y 0 w9@0x50 0x64 val1 val2 val3 r3 //如果设备地址不变,后面的设备地址可 省略 使用 I2C-Tools 操作传感器 AP3216C // 使用 SMBus 协议 i2cset -f -y 0 0x1e 0 0x4 i2cset -f -y 0 0x1e 0 0x3 i2cget -f -y 0 0x1e 0xc w i2cget -f -y 0 0x1e 0xe w // 使用 I2C 协议 i2ctransfer -f -y 0 w2@0x1e 0 0x4 i2ctransfer -f -y 0 w2@0x1e 0 0x3 i2ctransfer -f -y 0 w1@0x1e 0xc r2 i2ctransfer -f -y 0 w1@0x1e 0xe r2 10.5.2 I2C-Tools 访问 I2C 设备的 2 种方式 指定 I2C 控制器, i2c-dev.c 为每个 I2C 控制器(I2C Bus、I2C Adapter)都生成一个设备节点：/dev/i2c-0、/dev/i2c-1 等等. open 某个/dev/i2c-X 节点，就是去访问该 I2C 控制器下的设备 指定 I2C 设备. 通过 ioctl 指定 I2C 设备的地址, ioctl(file, I2C_SLAVE, address), ioctl(file, I2C_SLAVE_FORCE, address) 传输数据, I2C 方式：ioctl(file, I2C_RDWR, &rdwr), SMBus 方式：ioctl(file, I2C_SMBUS, &args) 10.5.3 源码流程分析 使用 I2C 方式, 示例代码：i2ctransfer.c 使用 SMBus 方式, 示例代码：i2cget.c、i2cset.c 10.6 编写 APP 直接访问 EEPROM Linux 驱动程序: drivers/i2c/i2c-dev.c, 参考 i2C-Tools 的代码 第11章 Linux 串口应用编程 Serial Programming Guide for POSIX Operating Systems Linux 串口编程 Linux 串口—struct termios 结构体 11.1 串口 API 需要设置行规程，比如波特率、数据位、停止位、检验位、RAW 模式、一有数据就返回 行规程的参数用结构体 termios 来表示， include\uapi\asm-generic\termios.h, struct termio #define NCC 8 struct termio { unsigned short c_iflag; /* input mode flags */ unsigned short c_oflag; /* output mode flags */ unsigned short c_cflag; /* control mode flags */ unsigned short c_lflag; /* local mode flags */ unsigned char c_line; /* line discipline */ unsigned char c_cc[NCC]; /* control characters */ }; 缩写，tc：terminal contorl , cf: control flag 常用函数，tcgetattr, tcsetattr, tcflush, cfsetispeed, cfsetospeed, cfsetspeed. 11.2 串口收发实验 回环收发