暂时未有相关云产品技术能力~
1. ASoC 概述ASoC (ALSA System on Chip) ,是建立在标准ALSA驱动层上,为了更好地支持嵌入式处理器和移动设备中的音频Codec的一套软件体系。在ASoc出现之前,内核对于SoC中的音频已经有部分的支持,不过会有一些局限性:Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码的重复。音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码重新对音频路劲进行配置。当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持通过改变过取样频率和偏置电流来达到省电的目的。ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树中:sound/soc。ASoC不能单独存在,他只是建立在标准ALSA驱动上的一个它必须和标准的ALSA驱动框架相结合才能工作。2. 硬件架构嵌入式设备的音频系统可以被划分为板载硬件(Machine)、Soc(Platform)、Codec三大部分,如下图所示:Machine :是指某一款机器,可以是某款设备,某款开发板,由此可以看出Machine几乎是不可重用的,每个Machine上的硬件实现可能都不一样,CPU不一样,Codec不一样,音频的输入、输出设备也不一样,Machine为CPU、Codec、输入输出设备提供了一个载体。Platform:一般是指某一个SoC平台,比如s3cxxxx,与音频相关的通常包含该SoC中的时钟、DMA、I2S、PCM等等,只要指定了SoC,那么我们可以认为它会有一个对应的Platform,它只与SoC相关,与Machine无关,这样我们就可以把Platform抽象出来,使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实际上,把Platform认为是某个SoC更好理解。Codec:字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、A/D、Mixer、PA(功放),通常包含多种输入(Mic、Line-in、I2S、PCM)和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样,是可重用的部件,同一个Codec可以被不同的Machine使用。嵌入式Codec通常通过I2C对内部的寄存器进行控制。注释:对于现在的很多嵌入式平台,内部集成了codec,我们在分析时也可以将其划分到codec 上,不同的Soc 内部Codec 有所不同,同时亦可兼容内部codec 和 外部Codec。3. 软件架构在软件层面,ASoC也把嵌入式设备的音频系统同样分为3大部分,Machine,Platform和Codec。Machine: Machine 驱动负责处理机器特有的一些控件和音频事件(例如,当播放音频时,需要先行打开一个放大器);单独的Platform 和 Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起才能完成整个设备的音频处理工作。Platform:它包含了该SoC平台的音频 DMA 和音频接口的配置和控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关的代码。Codec : ASoC 中的一个重要设计原则就是要求Codec驱动是平台无关的,它包含了一些音频的控件(Controls),音频接口,DAMP(动态音频电源管理)的定义和某些Codec IO功能。为了保证硬件无关性,任何特定于平台和机器的代码都要移到 Platform 和Machine驱动中。所有的Codec驱动都要提供以下特性:Codec DAI 和 PCM的配置信息;Codec的IO控制方式(I2C,SPI等);Mixer和其他的音频控件;Codec的ALSA音频操作接口;必要时,也可以提供以下功能:DAPM描述信息;DAPM事件处理程序;DAC数字静音控制4. ASOC 分析4.1 硬件抽象通常一个声卡设备,大概包含以下几个物理设备或者外设:Codec:音频编解码控制器,可以是内部Codec(soc 集成),也可以是外部Codec. Codec 通过支持音频编解码,包括模拟麦或者spk, 有的甚至支持数字麦。AMIC/SPK/DMIC:纯硬件电路。麦克风或者spk,软件无需干预。DMA:对于硬件设备的数据量,大多数情况都是通过dma 搬运来提高效率。cpu:整个soc 平台,主要提供音频通信接口来实现和codec 传输。比如(I2S/PDM等)DAI:音频接口,抽象概念,比如I2S等。Card:抽象概念,声卡。Capture:抽象概念,表示录音设备Playback:抽象概念,表示软件设备对于大多数平台,dma 和 i2s/pdm 等集成在一个soc 上,有些甚至集成了Codec。4.2 音频数据流音频数据的数据流,大致如下。我们可以看到,不同的硬件平台,其声卡设备的硬件逻辑和数据流大致一致,故抽象ASoc 很有必要。4.3 ASoc 软件抽象如下是笔者根据自己理解划分 Alsa 声卡驱动各个部分:Machine:驱动顶层和入口,处理声卡操作。包括声卡创建,音频流的传输与控制。platform:主要负责Soc 平台的DMA 和 CPU_DAI 操作。Codec:主要负责Codec driver 和 Codec_dai 操作。可以看到一个ALSA 声卡驱动是十分复杂的,包含了各种复杂驱动。codec driver:音频配置和传输dma:dma 处理dai:i2s等接口配置pcm:和上层应用交互的中间层control:和上层应用交互的中间层其他:比如i2c/spi ,codec 控制操作。4.4 ASoc 驱动分析我们以 linux-kernel-4.4.94 为例子来分析 ASoc 驱动。限于篇幅,我们只分析 Machine 驱动框架,对于Codec 驱动和其他设备驱动,有时间再分析。ASoc Machine 驱动调用如下:/* ASoC platform driver */ static struct platform_driver soc_driver = { .driver = { .name = "soc-audio", .pm = &snd_soc_pm_ops, }, .probe = soc_probe, .remove = soc_remove, };最顶层入口是soc_probe,位于 sound/soc/soc-core.c,不同的Machine 位置可能不同。大部分在soc/xxx/下。/* probes a new socdev */ static int soc_probe(struct platform_device *pdev) { struct snd_soc_card *card = platform_get_drvdata(pdev); /* ¦* no card, so machine driver should be registering card ¦* we should not be here in that case so ret error ¦*/ if (!card) return -EINVAL; dev_warn(&pdev->dev, ¦"ASoC: machine %s should use snd_soc_register_card()\n", ¦card->name); /* Bodge while we unpick instantiation */ card->dev = &pdev->dev; return snd_soc_register_card(card); }最关键的就是 snd_soc_register_card 这个函数了。详细分析看上图uml 时许图。4.5 ASoc 数据结构ASoc 数据结构如上图。最顶层我们构建了snd_soc_card。贯穿整个驱动生命周期中,snd_soc_pcm_runtime,至关重要。dai_link 关联着dai_driver 和 compoent_driversnd_soc_codec:codec 相关snd_soc_paltform:platform 相关snd_soc_dai:cpu_dai 和 codec_dai 相关操作snd_soc_component:关联dai_driver 和 component_driver。关联platform_driver 和 component_driversnd_soc_ops/snd_soc_dai_ops/snd_pcm_ops:比较关键的几个ops对于刚开始学习ASLA 驱动时,我们先关注这几个结构体就行。后续将从音频流和控制两大块,详细分析整个数据流和控制的调用过程。5. 总结本文详细的介绍了ALSA 驱动最关键的一环ASoC ,理解了ASoc 顶层设计框架对于我们后续深入学习ASLA 驱动至关重要。当我们熟悉了一个平台的驱动框架后,再去看另外一个平台就知道哪些是我们需要关注的,哪些是linux 内核已经实现的,从而达到事半功倍的作用。希望本文,对读者朋友学习理解Alsa 驱动有所帮助!
1.描述符布局如图为 bulk 传输描述符布局,相对于同步传输,批量传输只有一个可选择的配置,没有备用配置。VideoControl :无变化VideoStream:只有一个 bAlternateSetting(删除alt=1描述符)。同时支持bulk in 端点。需要修改的地方:static struct usb_interface_descriptor uvc_streaming_intf_alt0 = { .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, .bInterfaceNumber = UVC_INTF_VIDEO_STREAMING, .bAlternateSetting = 0, .bNumEndpoints = 1, /* alt0 挂一个bulk 端点*/ .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = UVC_SC_VIDEOSTREAMING, .bInterfaceProtocol = 0x00, .iInterface = 0, };端点描述符:static struct usb_endpoint_descriptor uvc_hs_streaming_ep = { .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_DIR_IN, .bmAttributes = USB_ENDPOINT_XFER_BULK, .wMaxPacketSize = 512, .bInterval = 0, };2. 控制流程根据USB规范可知,同步传输方式是只要带中带有同步端点的接口,系统会定时从设备中读取数据,无论设备中是否有数据。而如要停止数据的传输,只需要选中不带有同步端点的接口即可。USB同步传输这种灵活的数据传输方式是依靠视频流接口的转换接口即我们常说的备份接口实现的。在默认情况下数据不传输时,视频数据流接口和备份接口ID为0,其它的备份接口是可根据视频数据传输的大小可按需选择。我们知道,批量传输只有一个可选择的altsetting ,那么如何知道设备控制设备的开流和关流动作呢?2.1 stream on使用视频流接口的VS_COMMIT_CONTROL 提交给设备,让其以指定的数据格式进行数据采样。2.2 stream off关流操作,通过发送一个clear_halt 请求,来中断流的操作。2.3 代码分析基于 linux 4.14.281 内核版本:分析host 端uvc 开关流流程drivers/media/usb/uvc/uvc_queue.c开流操作:uvc_start_streamingstatic int uvc_start_streaming(struct vb2_queue *vq, unsigned int count) { struct uvc_video_queue *queue = vb2_get_drv_priv(vq); struct uvc_streaming *stream = uvc_queue_to_stream(queue); unsigned long flags; int ret; queue->buf_used = 0; ret = uvc_video_enable(stream, 1); if (ret == 0) return 0; spin_lock_irqsave(&queue->irqlock, flags); uvc_queue_return_buffers(queue, UVC_BUF_STATE_QUEUED); spin_unlock_irqrestore(&queue->irqlock, flags); return ret; }关流操作:uvc_stop_streamingstatic void uvc_stop_streaming(struct vb2_queue *vq) { struct uvc_video_queue *queue = vb2_get_drv_priv(vq); struct uvc_streaming *stream = uvc_queue_to_stream(queue); unsigned long flags; uvc_video_enable(stream, 0); spin_lock_irqsave(&queue->irqlock, flags); uvc_queue_return_buffers(queue, UVC_BUF_STATE_ERROR); spin_unlock_irqrestore(&queue->irqlock, flags); }重点关注:uvc_video_enable/* * Enable or disable the video stream. */ int uvc_video_enable(struct uvc_streaming *stream, int enable) { int ret; if (!enable) { uvc_uninit_video(stream, 1); if (stream->intf->num_altsetting > 1) { usb_set_interface(stream->dev->udev, stream->intfnum, 0); } else { /* UVC doesn't specify how to inform a bulk-based device * when the video stream is stopped. Windows sends a * CLEAR_FEATURE(HALT) request to the video streaming * bulk endpoint, mimic the same behaviour. */ unsigned int epnum = stream->header.bEndpointAddress & USB_ENDPOINT_NUMBER_MASK; unsigned int dir = stream->header.bEndpointAddress & USB_ENDPOINT_DIR_MASK; unsigned int pipe; pipe = usb_sndbulkpipe(stream->dev->udev, epnum) | dir; usb_clear_halt(stream->dev->udev, pipe); } uvc_video_clock_cleanup(stream); return 0; } ret = uvc_video_clock_init(stream); if (ret < 0) return ret; /* Commit the streaming parameters. */ ret = uvc_commit_video(stream, &stream->ctrl); if (ret < 0) goto error_commit; ret = uvc_init_video(stream, GFP_KERNEL); if (ret < 0) goto error_video; return 0; error_video: usb_set_interface(stream->dev->udev, stream->intfnum, 0); error_commit: uvc_video_clock_cleanup(stream); return ret; }分析代码可知:首先判断是否关流操作;如果是,判断接口的可选配置是否大于1,如果大于1,发送usb_set_interface(intfnum,0) 关流,否则发送usb_clear_halt 请求;如果是开流操作,发送commit 请求然后初始化 video/* * Initialize isochronous/bulk URBs and allocate transfer buffers. */ static int uvc_init_video(struct uvc_streaming *stream, gfp_t gfp_flags) { struct usb_interface *intf = stream->intf; struct usb_host_endpoint *ep; unsigned int i; int ret; stream->sequence = -1; stream->last_fid = -1; stream->bulk.header_size = 0; stream->bulk.skip_payload = 0; stream->bulk.payload_size = 0; uvc_video_stats_start(stream); if (intf->num_altsetting > 1) { struct usb_host_endpoint *best_ep = NULL; unsigned int best_psize = UINT_MAX; unsigned int bandwidth; unsigned int uninitialized_var(altsetting); int intfnum = stream->intfnum; /* Isochronous endpoint, select the alternate setting. */ bandwidth = stream->ctrl.dwMaxPayloadTransferSize; if (bandwidth == 0) { uvc_trace(UVC_TRACE_VIDEO, "Device requested null " "bandwidth, defaulting to lowest.\n"); bandwidth = 1; } else { uvc_trace(UVC_TRACE_VIDEO, "Device requested %u " "B/frame bandwidth.\n", bandwidth); } for (i = 0; i < intf->num_altsetting; ++i) { struct usb_host_interface *alts; unsigned int psize; alts = &intf->altsetting[i]; ep = uvc_find_endpoint(alts, stream->header.bEndpointAddress); if (ep == NULL) continue; /* Check if the bandwidth is high enough. */ psize = uvc_endpoint_max_bpi(stream->dev->udev, ep); if (psize >= bandwidth && psize <= best_psize) { altsetting = alts->desc.bAlternateSetting; best_psize = psize; best_ep = ep; } } if (best_ep == NULL) { uvc_trace(UVC_TRACE_VIDEO, "No fast enough alt setting " "for requested bandwidth.\n"); return -EIO; } uvc_trace(UVC_TRACE_VIDEO, "Selecting alternate setting %u " "(%u B/frame bandwidth).\n", altsetting, best_psize); ret = usb_set_interface(stream->dev->udev, intfnum, altsetting); if (ret < 0) return ret; ret = uvc_init_video_isoc(stream, best_ep, gfp_flags); } else { /* Bulk endpoint, proceed to URB initialization. */ ep = uvc_find_endpoint(&intf->altsetting[0], stream->header.bEndpointAddress); if (ep == NULL) return -EIO; /* Reject broken descriptors. */ if (usb_endpoint_maxp(&ep->desc) == 0) return -EIO; ret = uvc_init_video_bulk(stream, ep, gfp_flags); } if (ret < 0) return ret; /* Submit the URBs. */ for (i = 0; i < UVC_URBS; ++i) { ret = usb_submit_urb(stream->urb[i], gfp_flags); if (ret < 0) { uvc_printk(KERN_ERR, "Failed to submit URB %u " "(%d).\n", i, ret); uvc_uninit_video(stream, 1); return ret; } } /* The Logitech C920 temporarily forgets that it should not be adjusting * Exposure Absolute during init so restore controls to stored values. */ if (stream->dev->quirks & UVC_QUIRK_RESTORE_CTRLS_ON_INIT) uvc_ctrl_restore_values(stream->dev); return 0; }从这段代码可以看出,如果altsetting 大于1 走同步传输,发送usb_set_interface(intfnum, altsetting) ,选择合适带宽配置。然后初始化同步传输管道。否则,初始化 同步传输管道,提交传输。3. 其他注意点对比同步传输和批量传输我们可以发现,对于uvc 批量传输, 由于没有同步传输类似的多个可选配置,所以没法灵活控制开流关流操作。特别是在linux 平台下,要切换不同的格式和分辨率的时候没有同步传输方便。故,笔者觉得同步传输适合传固定数据,或者对usb camera 做中转使用比较合适。对于批量传输如果能充分发送usb 吞吐量,(USB2.0)一个微帧传输13个packet,理论带宽将近50MB/s, 笔者实际测试能达到47MB/s,对于YUYV图像能够极大提高帧率。
1. 指令集1.1 指令集指令集是一个CPU的基石,要实现CPU 计算和控制功能,就必须定义好一系列与硬件电路相匹配的指令系统.指令就是我们交代CPU 要执行的操作,指令集就可以简单理解为指令的集合。我们把cpu 能够识别的指令汇总在一起就构成了一个指令集。不同的CPU 有不同的指令集,根据他们的繁简程度可以分为两种:复杂指令集CISC 和精简指令集 RISC1.2 指令集架构指令架构(Instruction Set Architecture, 缩写为ISA),是软件和硬件的接口,不同的应用需求,会有不同的指令架构。要设计一款CPU 指令体系就是设计的出发点。2. RISC-V 指令集架构RISC-V 指令有以下特点:完全开放指令简单模块化设计,易于扩展名称类别说明RV32I基础指令整数指令:包含算法、分支、逻辑、访存指令,有32个32位寄存器。能寻址32位地址空间RV64I基础指令整数指令:包含算法、分支、逻辑、访存指令,有32个64位寄存器。能寻址64位地址空间RV128I基础指令整数指令:包含算法、分支、逻辑、访存指令,有32个128位寄存器。能寻址128位地址空间RV32E基础指令与RV32I一样,只不过只使用前16个(0~15)32位寄存器M扩展指令包含乘法、除法、取模求余指令F扩展指令单精度浮点指令D扩展指令双精度浮点指令Q扩展指令四倍精度浮点指令A扩展指令原子操作指令:比如比较并交换,读改写等指令C扩展指令压缩指令:单指令长度为16位,主要用于改善程序大小P扩展指令单指令多数据(Packed-SIMD)指令B扩展指令位操作指令H扩展指令支持(Hypervisor)管理指令J扩展指令支持动态翻译语言指令L扩展指令十进制浮点指令N扩展指令用户中断指令G通用指令包含I、M、A、F、D 指令要满足现在操作系统和应用程序的基本运行,RV32G指令集或者RV64G指令集就够了。RV32G和RV64G指令集只有寄存器位宽和寻址大小不同。这些指令按照功能可以分为如下几类:整数运算指令:算术、逻辑、比较等基础运算功能。分支转移指令:实现条件转移、无条件转移操作加载存储指令:实现字节、半字(half word)、字(word)、双字(RV64I)的加载,存储操作,采用的都是寄存器相对寻址方式控制与状态寄存器访问指令:实现对系统控制与系统状态寄存器的原子读-写、原子读-修改、原子读-清零等操作系统调用指令:实现系统调用功能。原子指令:用于各种同步锁单双浮点指令:实现浮点运算操作从上表我们可以看到,RISC-V 指令集具有模块化特点。这就允许我们根据自己的需求,选择一个基础指令集,加上若干个扩展指令集灵活搭配,就可以得到我们想要的指令集架构,进而根据这样的指令架构,设计出贴合我们需求的CPU.作为初学者,我们了解RISC-V 的核心即可。它的最核心部分是一个基础指令集,叫做RV32I.RV32I 包含的指令是固定不变的,这为编译器设计人员,操作系统开发人员和汇编语言程序员提供了稳定的基础框架。RV32I 指令集:RV32I 指令集如图所示,把带下划线的字母从左至右连接组合就是组成了RV32I指令。{}表示集合中垂直方向的每个项目指令不同变体。变体用下划线字母或者下划线表示表示,如果大括号里面只有下划线,则表示对此变体不需要用字母表示我们结合具体例子来看:下图表示了bge、blt、bgeu、bltu四个指令。3. 指令格式下图是RISC-V 指令格式,从下图可以看到RSIC-V共六种指令格式。opcode :指令操作码imm:代码立即数func3和funct7:代表指令对应的功能rs1:源寄存器1rs2:源寄存器2rd:目标寄存器(RSIC-V 一个指令可以提供三个寄存器操作)六种指令格式作用如下:序号指令类型作用1R 型指令用于寄存器和寄存器操作2I 型指令用于短立即数和内存载入指令load操作3S 型指令用于内存存储store操作4B 型指令用于有条件跳转操作5U 型指令用于长立即数操作6J 型指令用于无条件跳转操作4.寄存器在RISC-V 的规范里面定义了32 个通用寄存器。其中31个是常规寄存器,1个恒为0值的x0寄存器。0值寄存器是为了满足汇编语言程序员和编译器编写者的使用需要,他们可以使用x0寄存器作为操作数,来完成功能相同的操作。addi x0,x0,0 ; 0 = 0 + 0,相当于 nop 空指令RSIC-V 寄存器说明寄存器ABI 名称说明x0zero0值寄存器,硬编码为0,写入数据忽略,读取数据为0x1ra用于返回地址(return address)x2sp用于栈指针(stack pointer)x3gp用于通用指针 (global pointer)x4tp用于线程指针 (thread pointer)x5t0用于存放临时数据或者备用链接寄存器x6~x7t1~t2用于存放临时数据寄存器x8s0/fp需要保存的寄存器或者帧指针寄存器x9s1需要保存的寄存器x10~x11a0~a1函数传递参数寄存器或者函数返回值寄存器x12~x17a2~a7函数传递参数寄存器x18~x27s2-s11需要保存的寄存器x28~x31t3~t6用于存放临时数据寄存器5. RV32I 指令解读5.1 算术与逻辑指令在RV32I 中包括算术指令(add/sub)、数值比较指令(slt)、逻辑指令(and/or/xor)以及移位指令(sll/srl/sra)这几种指令。这些指令和其他指令集差不多,它们从寄存器读取两个32位的值,并将32位运算结果再写回到目标寄存器。I型指令:立即数算术运算R型指令:寄存器与寄存器操作指令需要指出的是,在寄存器与寄存器操作的算术指令中。必须要有减法指令。这和立即数操作指令不同。5.2 Load 和 Store 指令在RISC-V 指令集中,对内存的读写只能通过LOAD 和 STORE 指令实现。而其他的指令只能以寄存器为操作对象。如上图所示,load 和 store 的寻址模式只能是符号扩展12位的立即数,加上基地址寄存器得到访存的存储器地址。因为没有了复杂的内存寻址方式,这让CPU 流水线可以对数据冲突提前做出判断,并通过流水线各级转送加以处理,而不需要加入空操作(NOP),极大的提高了代码的执行效率。5.3 分支跳转指令5.3.1 有条件的分支跳转RV32I 中的条件跳转就是通过比较两个寄存器的值,进行分支跳转:beq:相等bne:不相等bge/bgeu:大于等于blt/bltu:小于5.3.2 无条件的分支跳转无条件跳转指令可以细分为直接跳转和间接跳转。直接跳转指令JAL 如下图所示:JAL 指令执行过程是这样的。它会把20位立即数做符号位扩展。并左移一位,产生一个32位符号数。然后,将该32位符号数和PC相加来产生目标地址(这样,JAL 可以作为短跳转指令,跳至PC+1MB的地址范围内)同时JAL 会把紧随其后的那条指令地址,存入目标寄存器中。这样,如果目标寄存器是零,则JAL就等同GOTO指令;如果目标寄存器不为零,JAL可以实现函数调用功能。间接跳转直接JALR如下:JALR指令会把12位立即数和源寄存器相加,并把相加结果末位清零,作为新的跳转地址。同时和JAL指令一样,也会把紧随其后的那条指令地址,存入目标寄存器中。5.4 其他指令除了内存地址空间和通用寄存器地址空间外,RISC-V 还定义了一个独立的控制和状态寄存器地址空间(Control Status Register) 每个处理器实现的CSR会因设计目标不同而有差异,但是这些CSR的访问方式却是一样的,访问这些CSR指令定义在了用户指令集中(Zicsr指令集扩展)有了上图这些CSR 指令,能够让我们轻松的访问一些程序性能计数器。这些计数器包括系统时间、时间周期以及执行的指令数目。延伸阅读:RISC-V-Reader-Chinese-v2p1.pdf
1. 芯片芯片 是所有半导体元器件的统称,它是把一定数量的常用电子元件(如电阻,电容,晶体管等),通过半导体工艺集成在一起,具有特定功能的电路。2. CPUcpu 是芯片的一种,它里面包含了控制部件和运算部件,即中央处理器。1971 年, Intel 将运算器和控制器集成到一个芯片上,称为4004 微处理器,这标志着CPU 的诞生。CPU 的工作流程分为以下5个阶段:取指令指令译码执行指令访存读取数据结果写回指令和数据统一存储在内存中,数据与指令需要从统一的存储空间存取,经由共同的总线传输,无法并行读取数据和指令。冯诺依曼结构3. 冯诺依曼结构冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同,如英特尔公司的8086中央处理器的程序指令和数据都是16位宽。数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成:运算器控制器存储器输入设备输出设备现代计算机发展所遵循的基本结构形式始终是冯·诺依曼机结构。这种结构特点是“程序存储,共享数据,顺序执行”,需要 CPU 从存储器取出指令和数据进行相应的计算(1)单处理机结构,机器以运算器为中心; (2)采用程序存储思想; (3)指令和数据一样可以参与运算; (4) 数据以二进制表示; (5)将软件和硬件完全分离; (6) 指令由操作码和操作数组成; (7)指令顺序执行。这套理论被称为冯·诺依曼体系结构。4. 哈佛结构哈佛结构是一种将程序指令存储和数据存储分开的存储器结构,如下图所示。中央处理器首先到程序指令存储器中读取程序指令内容,解码后得到数据地址,再到相应的数据存储器中读取数据,并进行下一步的操作(通常是执行)。程序指令存储和数据存储分开,可以使指令和数据有不同的数据宽度,如Microchip公司的PIC16芯片的程序指令是14位宽度,而数据是8位宽度。哈佛结构的微处理器通常具有较高的执行效率。其程序指令和数据指令分开组织和存储的,执行时可以预先读取下一条指令。哈佛结构是指程序和数据空间独立的体系结构, 目的是为了减轻程序运行时的访存瓶颈。哈佛结构能基本上解决取指和取数的冲突问题。5. 混合式结构使用两个独立的存储器模块,分别存储指令和数据,每个存储模块都不允许指令和数据并存;具有一条独立的地址总线和一条独立的数据总线,利用公用地址总线访问两个存储模块(程序存储模块和数据存储模块),公用数据总线则被用来完成程序存储模块或数据存储模块与CPU之间的数据传输;两条总线由程序存储器和数据存储器分时共用。6. CISC从前面的内容中,我们已经得知 CPU 就是不断的执行指令,来实现程序的执行,最后实现相应的功能。但是一颗CPU 能实现多少条指令,每条指令完成多少功能,却是值得细细考量的问题。CISC的英文全称为“Complex InstrucTIon Set Computer”,即“复杂指令系统计算机”,从计算机诞生以来,人们一直沿用CISC指令集方式。早期的桌面软件是按CISC设计的,并一直沿续到现在。目前,桌面计算机流行的x86体系结构即使用CISC。CISC 的优势在于,用少量的指令就能实现非常多的功能,程序自身大小也会下降,减少内存空间占用。缺点:这些复杂指令集,包含的指令数量多且功能复杂,设计复杂。7. RISCRISC的英文全称为“Reduced InstrucTIon Set Computer”,即“精简指令集计算机”,是一种执行较少类型计算机指令的微处理器,起源于80年代的MIPS主机(即RISC机),RISC机中采用的微处理器统称RISC处理器。这样一来,它能够以更快的速度执行操作(每秒执行更多百万条指令,即MIPS)。因为计算机执行每个指令类型都需要额外的晶体管和电路元件,计算机指令集越大就会使微处理器更复杂,执行操作也会更慢。RISC 设计方案非常简约,通常有20 多条简化的指令集。每条指令长度固定,由专用的加载和储存指令用于访问内存,减少了内存的寻址方式,大多数运算指令只能访问操作寄存器。CPU 中配有大量的寄存器,这些指令的选取都是工程中使用频率最高的指令。由于指令长度一致,功能单一,操作依赖于寄存器,这些特性使得CPU 指令预取、分支预测、指令流水线等部件的效能大大发挥,几乎一个时钟周期能执行多条指令RISC 的代表产品是 ARM 和 RISC-V。现在,两者已经没有明显的界限了,开始相互融合了8. 流水线谈到指令并行,就不得不谈到CPU 核心的流水线。现代处理器都是流水线结构。流水线(Pipeline)技术是指程序在执行时候多条指令重叠进行操作的一种准并行处理实现技术。通俗的讲将一个时序过程,分解成若干个子过程,每个过程都能有效的与其他子过程同时执行。这种思想最初是在RISC的架构中出现的,旨在提高处理器处理效率,争取在一个时钟周期中完成一条指令。一般常见的5级流水线有:取指:指令取指(Instruction Fetch)是指将指令从存储器中读取出来的过程。译码:指令译码(Instruction Decode)是指将存储器中取出的指令进行翻译的过程。经过译码之后得到指令需要的操作数寄存器索引,可以使用此索引从通用寄存器组(Register File)中将操作数读出。执行:指令译码之后所需要进行的计算类型都已得知,并且已经从通用寄存器组中读取出了所需的操作数,那么接下来便进行指令执行(Instruction Execute)。指令执行是指对指令进行真正运算的过程。譬如,如果指令是一条加法运算指令,则对操作数进行加法操作;如果是减法运算指令,则进行减法操作。在“执行”阶段的最常见部件为算术逻辑部件运算器(Arithmetic Logical Unit,ALU),作为实施具体运算的硬件功能单元。访存:存储器访问指令往往是指令集中最重要的指令类型之一,访存(Memory Access)是指存储器访问指令将数据从存储器中读出,或者写入存储器的过程。写回:写回(Write-Back)是指将指令执行的结果写回通用寄存器组的过程。如果是普通运算指令,该结果值来自于“执行”阶段计算的结果;如果是存储器读指令,该结果来自于“访存”阶段从存储器中读取出来的数据。无流水线:有流水线:它增加了四组寄存器,每一个流水线级数内部都有各自的组合逻辑数据通路,彼此之间没有复用资源,因此,其面积开销是比较大的,但是由于可以让不同的流水线级数同时做不同的事情,而达到流水的效果,提高了性能,优化了时序,增加了吞吐率。9. RISC-V在了解了 RISC 和 CISC 两种计算机指令设计架构后。我们来看看 RISC-V。RISC-V 的 “V”, 有两层意思,一方面代表第5代 RISC;另一方面, “V”取Variation 之意代表变化。9.1 RISC-V 是什么?RISC-V 是一套开放许可证书、免费的、由基金维护的、一个整数运算指令集外加多个扩展指令集的CPU 结构规范(ISA)。整数运算指令集 + 扩展指令集任何硬件开发商或者组织都可以免费使用这套规范,构建CPU 芯片产品。9.2 指令集命名方式以RV 为2前缀,然后是位宽,最后代表是指令集的字母集合:RV[###][abc......xyz]符号说明RVRISC-V 缩写[###]用于标识处理器位宽,取值[32, 64,128],也就是处理器的寄存器位宽[abc...xyz]标识该处理器支持的指令模块集合比如:RV64IMAC, 表示64 位 RISC-V, 支持整数指令、乘除法指令、原子指令和压缩指令。9.3 指令集模块指令集模块是一款CPU架构的主要组成部分,是CPU 和 上层软件交互的核心,也是cpu主要功能体现。RISC-V 规范只定义了CPU 需要包含的基础整型操作指令:整型的储存加载加减逻辑移位分支等。其他指令为可选指令或者用户扩展指令。比如:乘除取模单精度浮点双精度浮点压缩原子指令等。扩展指令是芯片工程师根据需求自定义。所以 RISC-V 采用的是模块化的指令集,易于扩展、组装。它适用于不同的应用场景,可以降低 CPU 实现成本。9.4 RISC-V 寄存器指令的操作数来源于寄存器,精简指令架构的CPU,都会提供大量的寄存器。RISC-V 的规范定义了32个通用寄存器以及一个PC寄存器,这对于RV32I、RV64I、RV128I 指令集都是一样的,只是寄存器的位宽不一样。如果要实现支持F/D扩展指令集的CPU,则需要额外支持32个浮点寄存器。而如果实现只支持RV32E指令集的嵌入式CPU,则可以将32个通用寄存器缩减为16个通用寄存器。寄存器ABI 名称说明x0zero0值寄存器,硬编码为0,写入数据忽略,读取数据为0x1ra用于返回地址(return address)x2sp用于栈指针(stack pointer)x3gp用于通用指针 (global pointer)x4tp用于线程指针 (thread pointer)x5t0用于存放临时数据或者备用链接寄存器x6~x7t1~t2用于存放临时数据寄存器x8s0/fp需要保存的寄存器或者帧指针寄存器x9s1需要保存的寄存器x10~x11a0~a1函数传递参数寄存器或者函数返回值寄存器x12~x17a2~a7函数传递参数寄存器x18~x27s2-s11需要保存的寄存器x28~x31t3~t6用于存放临时数据寄存器ABI: 应用程序二进制接口,可以理解为寄存器别名,高级语言在生成汇编会用到。9.5 RSIC-V 特权级不同的 指令集架构都有特权级的概念,RSIC-V 也不例外,我们来看看RISC-V 的特权级。不同的特权级能访问的系统资源不同,高特权级的能访问低特权级的资源,反之却不行。RISC-V 的规范文档定义了四个特权级别(privilege level),特权等级由高到低排列,如下表所示。名称级别缩写编码用户,应用程序特权级0U00管理员特权级1S01虚拟机监视特权级2H10机器特权级3M11一个RISC-V 硬件线程(hart),相当于一个CPU 内独立的可执行核心,在任意时刻,只能运行在某一个特权级上,这个特权级由CSR(控制和状态寄存器)指定配置。具体分级如下:机器特权级(M):RISC-V 中 hart 可以执行的最高权限模式。在M 模式下运行的 hart,对内存、I/O 和一些必要的底层功能(启动和系统配置)有着完全的控制权。它是唯一一个所有标准RISC-V CPU 都必须实现的权限级。虚拟机监视特权级(H):为了支持虚拟机监视器而定义的特权级。管理员特权级(S):主要用于支持现代操作系统,如Linux、FreeBSD和 windows 等用户应用特权级(U):用于运行应用程序,同样也适用于嵌入式系统。特权级的存在,是给指令加上了权力,从而去控制指令编写应用程序。应用程序只能干应用程序该干的事情,不能越权操作。操作系统则拥有更高的权力,能对系统资源进行管理。10. 总结本文梳理了 芯片、CPU、流水线,指令与架构等基础概念,引出了RISC-V 基础介绍,简单介绍了RISC-V 由来。后续针对risc-v 会根据自身学习情况做相应介绍。参考文档: riscv-privileged-20190608.pdf riscv-spec-20191213.pdf RISC-V-Reader-Chinese-v2p1.pdf
1. 错误异常epc : 0x802cb3bcra : 0x802cb3a82. 调试方法2.1.反编译vmlinuxmips-linux-gnu-objdump -d vmlinux > dump.s2.2. 打开dump.s,找到你要找的epc值802cb3b4: 8e020004 lw v0,4(s0) 802cb3b8: 24420800 addiu v0,v0,2048 802cb3bc: ac620000 sw v0,0(v1) # 这里是异常 802cb3c0: 0000000f sync 802cb3c4: 8e050588 lw a1,1416(s0) 802cb3c8: 3c04804d lui a0,0x804d2.3. 向上找找这行指令在哪个函数?802cb298 <dwc2_hsotg_core_init_disconnected>: 802cb298: 27bdffe0 addiu sp,sp,-32 802cb29c: afb10018 sw s1,24(sp) 802cb2a0: afb00014 sw s0,20(sp) 802cb2a4: afbf001c sw ra,28(sp)2.4.找到它在哪个文件此接口位于:drivers/usb/dwc2/gadget.c2.5. touch一下这个c文件touch drivers/usb/dwc2/gadget.c这步是为了摸一下它,好让此文件在不改动的情况下重新编译一次,这样做的目的是,获取详细反汇编信息。2.6. make V=1,取此文件的编译命令make uImage V=1 -j16 > aaa.txt打开 aaa,txt2.7.单独编译此文件在命令行粘贴此命令(一个字符都不能少),后面加上(空格)--save-temps -g,回车,发现当前路径下多了俩文件:gadget.i gadget.s。前者是预编译文件,后者也是我们需要的——汇编文件2.8.打开.s文件找到之前要找的的点根据之前定位的函数:dwc2_hsotg_core_init_disconnecteddwc2_hsotg_core_init_disconnected: .frame $sp,32,$31 # vars= 0, regs= 3/0, args= 16, gp= 0 .mask 0x80030000,-4 .fmask 0x00000000,0 $LVL835 = . addiu $sp,$sp,-32 .cfi_def_cfa_offset 32 sw $17,24($sp) sw $16,20($sp) sw $31,28($sp) .cfi_offset 17, -8 .cfi_offset 16, -12 .cfi_offset 31, -4 .loc 1 3337 0 move $17,$5 .loc 1 3342 0 lw $5,1348($4)对比内核 dump.s802cb298 <dwc2_hsotg_core_init_disconnected>: 802cb298: 27bdffe0 addiu sp,sp,-32 802cb29c: afb10018 sw s1,24(sp) #是否发现这两处的指令是对应的?这时当然的。 之后就可以寻找**0x802cb3bc**这个点在gadget.s中的位置,切记,前后一定要严格对应,要不就找错了,如,在dump.s中,这点在这: ... 802cb398: 8e050588 lw a1,1416(s0) 802cb39c: 3c04804d lui a0,0x804d 802cb3a0: 0c02958b jal 800a562c <printk> 802cb3a4: 2484f41c addiu a0,a0,-3044 802cb3a8: 8e020588 lw v0,1416(s0) 802cb3ac: 10400009 beqz v0,802cb3d4 <dwc2_hsotg_core_init_disconnected+0x13c> 802cb3b0: 3c030080 lui v1,0x80 802cb3b4: 8e020004 lw v0,4(s0) 802cb3b8: 24420800 addiu v0,v0,2048 802cb3bc: ac620000 sw v0,0(v1) // 真正的异常 802cb3c0: 0000000f sync 802cb3c4: 8e050588 lw a1,1416(s0) 802cb3c8: 3c04804d lui a0,0x804d 802cb3cc: 0c02958b jal 800a562c <printk>对应gadget.s 异常如下:#NO_APP $LBE2718 = . .loc 1 3394 0 lw $5,1416($16) lui $4,%hi($LC40) .set noreorder .set nomacro jal printk addiu $4,$4,%lo($LC40) .set macro .set reorder $LVL843 = . .loc 1 3395 0 lw $2,1416($16) .set noreorder .set nomacro beq $2,$0,$L982 li $3,8388608 # 0x800000 .set macro .set reorder .loc 1 3396 0 lw $2,4($16) addiu $2,$2,2048 $LBB2722 = . $LBB2723 = . $LBB2724 = . $LBB2725 = . .loc 2 429 0 sw $2,0($3) // epc 0x0x802cb3bc 异常快速找到它的秘诀是,看到 sw v0,0(v1),就搜sw 3),然后再看看上下指令是否对应。找到这处,做个标记。寄存器辅助记忆:2.9. 找到c文件中的对应地方$LBB2722 = . $LBB2723 = . $LBB2724 = . $LBB2725 = . .loc 2 429 0 sw $2,0($3) // epc 0x0x802cb3bc 异常 $L1008: $LBE2725 = . $LBE2724 = . $LBE2723 = . $LBE2722 = . $LBB2726 = . $LBB2727 = . .loc 3 79 0注意他前面的.loc 2 429 0,这就是他的藏身所在。.loc 1 意思是第一个文件,可以从gadget.s中搜索file 1,如:.file 2 "./arch/mips/include/asm/io.h"429:行号 这就是它在的文件。打开它,找到 429结合下文的.loc 3 79 0。.file 3 "drivers/usb/dwc2/core.h"79: 行号 最终位置:dwc2_hsotg_core_init_disconnected // drivers/usb/dwc2/gadget.c dwc2_writel(hsotg->regs + DCFG, DCFG_DESCDMA_EN); __raw_writel(value, addr); // drivers/usb/dwc2/core.h +79可以看作最后出错的指令 sw v0,0(v1), 结合代码大致定位到操作DCFG 寄存器出错。对于硬件寄存器,尽量按位操作,也就是只操作特定比特位,否则会引入未知风险3. 总结保存错误现场(epc/ra地址)反汇编内核:objdump -d vmlinux > dump.s根据dump.s 找到指定 epc 值根据epc 值 确定发生异常的函数根据函数名确定发生异常的文件重新编译发生异常的文件,得到详细汇编和预处理信息(--save-temps -g)根据epc 确定异常位置。根据异常位置,定位文件名及行号。一般经过这几步就可以精确定位错误发生地方了。对于发生异常的函数名定位,有的时候根据经验可以直接通过内核trace dump 信息找到。或者根据epc 直接在 system.map 也可以定位。对于内核反汇编和重新编译异常文件,依赖于读者对汇编指令的理解。特别是对比 dump.s 和 gadget.s ,如果快速定位和找对很关键。
1. 什么是 libusblibusb是一个提供对 USB 设备的通用访问的 C 库。它旨在供开发人员用于促进与 USB 硬件通信的应用程序的生产。可移植的:使用单一的跨平台 API,它提供对 Linux、macOS、Windows 等 USB 设备的访问用户模式:应用程序与设备通信不需要特殊特权或提升。与usb 版本无关:支持所有版本的 USB 协议,从 1.0 到 3.1(最新)。跨平台:Linux、macOS、Windows(Vista 和更新版本)、Android、OpenBSD/NetBSD、Haiku、Solaris。2. 常用接口介绍libusb 接口可以参考官网wikihttps://libusb.sourceforge.io/api-1.0/支持所有传输方式:中断、控制、同步、批量传输接口:Synchronous (简单),Asynchronouslibusb 模块如下:设备初始化;设备控制与枚举;描述符处理;热插拔事件;同步I/O;异步I/O;其他处理;2.1 初始化与反初始化/* * @brief : libusb 初始化。 * @param: 参数可选,默认为NULL。 */ int libusb_init(libusb_context ** ctx); void libusb_exit(libusb_context * ctx);2.2 设备控制和枚举2.2.1 枚举设备/* * @brief:获取设备列表 * @param: * ctx:上下文,可选 * list:枚举的设备list * @return: 返回设备数量 */ ssize_t libusb_get_device_list (libusb_context *ctx, libusb_device ***list) /* * @brief:释放设备列表 * @param: * list:枚举的设备list * unref_devices:是否取消引用列表中的设备 * @return: no */ void libusb_free_device_list (libusb_device **list, int unref_devices); /* * @brief: 获取设备信息 * @param: * dev_handle:设备句柄 * @return: 返回底层设备信息 */ ibusb_device * libusb_get_device (libusb_device_handle *dev_handle); /* * @brief: 获取配置信息 * @param: * dev_handle:设备句柄 * config:活动配置的 bConfigurationValue * @return: 0 */ int libusb_get_configuration (libusb_device_handle *dev_handle, int *config); /* * @brief: 设备配置 * 如果您在已经配置了所选配置的设备上调用此函数, * 则此函数将充当轻量级设备重置: * 它将使用当前配置发出 SET_CONFIGURATION 请求, * 导致大多数与USB 相关的设备状态被重置(altsetting reset为零,端点停止清 * 除,切换复位。一般不手动调用。 * @param: * dev_handle:设备句柄 * config:活动配置的 bConfigurationValue * @return: 0 */ int libusb_set_configuration (libusb_device_handle *dev_handle, int configuration);2.2.2 设备控制/* * @brief: 打开设备 * @param: * dev:要打开的设备 * dev_handle:设备句柄 * @return: 0 */ int libusb_open (libusb_device *dev, libusb_device_handle **dev_handle); /* * @brief:打开设备 * @param: * ctx:上下文,默认为NULL * vendor_id:vid * product_id:pid * @return: 返回设备句柄 */ libusb_device_handle * libusb_open_device_with_vid_pid (libusb_context *ctx, uint16_t vendor_id, uint16_t product_id); void libusb_close (libusb_device_handle *dev_handle); /* * @brief: 复位设备 * 执行 USB 端口重置以重新初始化设备。重置完成后, * 系统将尝试恢复之前的配置和备用设置。 * 这是一个阻塞功能,通常会导致明显的延迟。 * @param: * dev_handle:设备句柄 * @return: 0 */ int libusb_reset_device (libusb_device_handle *dev_handle);2.2.3 接口处理接口激活与绑定:/* * @brief :接口驱动激活。 * 确定内核驱动程序是否在接口上处于活动状态。 * 如果内核驱动程序处于活动状态,则无法声明接口,libusb 将无法执行 I/O。 * 此功能在 Windows 上不可用。 * @param: * dev_handle:设备句柄 * interface_number:接口号 * @return: 0 */ int libusb_kernel_driver_active (libusb_device_handle *dev_handle, int interface_number); /* * @brief :从接口中分离内核驱动程序。 * 如果成功,您将能够声明接口并执行 I/O。 * 此功能在 Windows 上不可用。 * @param: * dev_handle:设备句柄 * interface_number:接口号 * @return: * 0: 接口处于非活动状态 * 1:接口处于活动状态 */ int libusb_detach_kernel_driver (libusb_device_handle *dev_handle, int interface_number); /* * @brief :重新附加接口的内核驱动程序, * 该驱动程序以前使用libusb_detach_kernel_driver()分离。 * 此功能在 Windows 上不可用。 * @param: * dev_handle:设备句柄 * interface_number:接口号 * @return: 0 */ int libusb_attach_kernel_driver (libusb_device_handle *dev_handle, int interface_number); /* * @brief :启用/禁用 libusb 的自动内核驱动程序分离。 * 启用此选项后,libusb 将在声明接口时自动分离接口上的内核驱动程序, * 并在释放接口时附加它。 * 默认情况下,在新打开的设备句柄上禁用自动内核驱动程序分离。 * @param: * dev_handle:设备句柄 * enable:使能 * @return: 0 */ int libusb_set_auto_detach_kernel_driver (libusb_device_handle *dev_handle, int enable);接口声明和配置:/* * @brief :声明一个接口。 * 您必须先声明您希望使用的接口,然后才能在其任何端点上执行 I/O。 * 声明接口是纯逻辑操作;它不会导致通过总线发送任何请求。 * 接口声明用于指示您的应用程序希望获得接口所有权的底层操作系统 * 如果 auto_detach_kernel_driver 设置为 1 dev,内核驱动程序将在必要时deattach,失败时返回deattach 错误。 * 非阻塞函数 * @param: * dev_handle:设备句柄 * interface_number:接口号 * @return: 0 */ int libusb_claim_interface (libusb_device_handle *dev_handle, int interface_number); /* * @brief:释放声明的接口。 * 您应该在关闭设备句柄之前释放所有声明的接口。 * 将向设备发送 SET_INTERFACE 控制请求,将接口状态重置* 为第一个备用设置。 * 如果 auto_detach_kernel_driver 设置为 1 dev,则释放接口后内核驱动程序将重新attach。 * 阻塞函数 * @param: * dev_handle:设备句柄 * interface_number:接口号 * @return: 0 */ int libusb_release_interface (libusb_device_handle *dev_handle, int interface_number); /* * @brief: 激活接口的备用设置。该接口必须先前已使用libusb_claim_interface()声明。 * 您应该始终使用此函数,而不是制定自己的 SET_INTERFACE 控制请求。 * 这是因为底层操作系统需要知道何时发生此类更改。 * 阻塞函数。 * @param: * dev_handle:设备句柄 * interface_number:接口号 * alternate_setting:可选配置 * @return: 0 */ int libusb_set_interface_alt_setting (libusb_device_handle *dev_handle, int interface_number, int alternate_setting);端点:/* * @brief: 清除端点的停止/停止条件。 * 在停止条件停止之前,处于停止状态的端点无法接收或传输数据。 * 阻塞函数 * @param: * dev_handle:设备句柄 * endpoint:端点地址 * @return: 0 */ int libusb_clear_halt (libusb_device_handle *dev_handle, unsigned char endpoint);2.3 USB 描述符2.3.1 设备/* * @brief: 获取给定设备的 USB 设备描述符。 * 这是一个非阻塞函数;设备描述符缓存在内存中。 * @param: * dev:设备 * desc:设备描述符 * @return: 0 */ int libusb_get_device_descriptor (libusb_device *dev, struct libusb_device_descriptor *desc);2.3.2 配置/* * @brief: 获取当前活动配置的 USB 配置描述符。 * 这是一个非阻塞功能,不涉及向设备发送任何请求。 * @param: * dev:设备 * config:配置描述符。使用后必须使用libusb_free_config_descriptor()释放。 * @return: 0 */ int libusb_get_active_config_descriptor (libusb_device *dev, struct libusb_config_descriptor **config); /* * @brief: 根据索引获取 USB 配置描述符。 * 这是一个非阻塞功能,不涉及向设备发送任何请求。 * @param: * dev:设备 * config_index:配置索引 * config:配置描述符。使用后必须使用 libusb_free_config_descriptor()释放。 * @return: 0 */ int libusb_get_config_descriptor (libusb_device *dev, uint8_t config_index, struct libusb_config_descriptor **config); /* * @brief: 获取具有特定 bConfigurationValue 的 USB 配置描述符。 * 这是一个非阻塞功能,不涉及向设备发送任何请求。 * @param: * dev:设备 * bConfigurationValue:检索的配置的 bConfigurationValue * config:配置描述符。使用后必须使用 libusb_free_config_descriptor()释放。 * @return: 0 */ int libusb_get_config_descriptor_by_value (libusb_device *dev, uint8_t bConfigurationValue, struct libusb_config_descriptor **config); /* * @brief: 释放配置描述符 * @param: * config:配置描述符. * @return: no */ void libusb_free_config_descriptor(struct libusb_config_descriptor *config);2.3.3 其他描述符bos 描述符:int libusb_get_bos_descriptor (libusb_device_handle *dev_handle, struct libusb_bos_descriptor **bos); void libusb_free_bos_descriptor (struct libusb_bos_descriptor *bos);usb 2.0 扩展:int libusb_get_usb_2_0_extension_descriptor (libusb_context *ctx, struct libusb_bos_dev_capability_descriptor *dev_cap, struct libusb_usb_2_0_extension_descriptor **usb_2_0_extension); void libusb_free_usb_2_0_extension_descriptor (struct libusb_usb_2_0_extension_descriptor *usb_2_0_extension)字符串描述符:int libusb_get_string_descriptor_ascii (libusb_device_handle *dev_handle, uint8_t desc_index, unsigned char *data, int length)2.4 热插拔事件/* * @brief: 注册一个热插拔回调函数 * @param: * ctx:注册此回调的上下文 * events:触发此回调的热插拔事件 * flags:热插拔标志 * vendor_id:vid * product_id:pid * dev_class:设备类 * cb_fn:回调函数 * user_data:传递给回调函数的用户数据 * callback_handle:存储已分配的回调句柄指针 * @return: 0 */ int libusb_hotplug_register_callback ( libusb_context * ctx, int events, int flags, int vendor_id, int product_id, int dev_class, libusb_hotplug_callback_fn cb_fn, void * user_data, libusb_hotplug_callback_handle * callback_handle ); /* * @brief: 注销一个热插拔回调函数 * @param: * ctx:注册此回调的上下文 * callback_handle:存储已分配的回调句柄指针 * @return: 0 */ void libusb_hotplug_deregister_callback ( libusb_context * ctx, libusb_hotplug_callback_handle callback_handle ); /* * @brief: 获取与热插拔回调关联的 user_data。 * @param: * ctx:注册此回调的上下文 * callback_handle:存储已分配的回调句柄指针 * @return: 0 */ void* libusb_hotplug_get_user_data( libusb_context * ctx, libusb_hotplug_callback_handle callback_handle )‘2.5 同步I/O2.5.1 控制传输/* * @brief: usb 控制传输 * @param: * dev_handle: 设备句柄 * bmRequestType: 请求类型 * bRequest: 请求字段 * wValue: 值字段 * wIndex: 索引字段 * data: 请求数据 * wLength: 数据包长度 * timeout: 超时(ms), 0为阻塞等待 * @return 返回实际传输的字节 */ int libusb_control_transfer( libusb_device_handle * dev_handle, uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char * data, uint16_t wLength, unsigned int timeout );3. 下载与使用官网:https://libusb.info/开发文档:https://libusb.sourceforge.io/api-1.0/下载:最新稳定版本源码下载:https://github.com/libusb/libusb编译与使用:在pc 端使用libusb 可以下载二进制包,也可用源码编译。linux 下源码编译库#编译前准备 git clone https://github.com/libusb/libusb.gitsudo apt-get install autoconf sudo apt-get install libtool sudo apt-get install libudev-dev编译:cd libusb ./configure --prefix=$HOME/github/libusb/out make make install直接将对应的库移植到相应的工程下。4. 常规使用流程初始化libusb usb 库打开指定设备声明某个接口设置接口绑定和分离复位设备读写设备关闭设备关闭库4.1 初始化void CLibusbControl::bulkusb_init() { uint16_t pid = 0; uint16_t vid = 0; int ret = -1; /*Step1: 初始化 */ libusb_init(NULL); vid = m_usbid.vid; pid = m_usbid.pid; /*Step2:打开设备 */ libusb_device_handle* pHandle = libusb_open_device_with_vid_pid(NULL, vid, pid); if (pHandle == NULL) { printf("libusb_open_device_with_vid_pid(%d, %d) failed \n", vid, pid); libusb_exit(NULL); return; } m_usbdev.handle = pHandle; /*Step3: 声明某个接口*/ int iface = get_interface_attr(pHandle); if (iface < 0) { libusb_close(pHandle); libusb_exit(NULL); } libusb_set_auto_detach_kernel_driver(pHandle, 1); libusb_reset_device(pHandle); }声明接口:int CLibusbControl::get_interface_attr(libusb_device_handle* handle) { int ret = -1; /*Step1: 获取设备描述符 */ libusb_device* dev = NULL; dev = libusb_get_device(handle); if (dev == NULL) { printf("libusb_get_device error\n"); return -1; } struct libusb_device_descriptor desc; ret = libusb_get_device_descriptor(dev, &desc); if (ret < 0) { fprintf(stderr, "failed to get device descriptor"); return -1; } /* Step2: 获取配置描述符 */ struct libusb_config_descriptor* conf_desc; const struct libusb_endpoint_descriptor* endpoint; uint8_t endpoint_in = 0, endpoint_out = 0; // default IN and OUT endpoints int nb_ifaces; uint8_t interfaceClass; ret = libusb_get_config_descriptor(dev, 0, &conf_desc); if (ret < 0) { fprintf(stderr, "failed to get config descriptor"); return -1; } /* Step3:获取接口描述符 */ nb_ifaces = conf_desc->bNumInterfaces; m_usbdev.nb_ifaces = nb_ifaces; for(int i = 0; i < nb_ifaces; i++) { for (int j = 0; j < conf_desc->interface[i].num_altsetting; j++) { //只获取接口类别为:厂商自定义类(0xFF)和CDC数据类(0xA) interfaceClass = conf_desc->interface[i].altsetting[j].bInterfaceClass; if (interfaceClass != 0xFF) { //0xA CDC continue; } m_usbdev.bulk_interface = i; for (int k = 0; k < conf_desc->interface[i].altsetting[j].bNumEndpoints; k++) { endpoint = &conf_desc->interface[i].altsetting[j].endpoint[k]; // Use the first bulk IN/OUT endpoints as default for testing if ((endpoint->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) & (LIBUSB_TRANSFER_TYPE_BULK)) {//只获取批量传输端点 if (endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) { if (!endpoint_in) { endpoint_in = endpoint->bEndpointAddress; m_usbdev.endpoint_in = endpoint_in;//赋值 // libusb_clear_halt(handle, bdev->endpoint_in);//清除暂停标志 } } else { if (!endpoint_out) { endpoint_out = endpoint->bEndpointAddress; m_usbdev.endpoint_out = endpoint_out;//赋值 // libusb_clear_halt(handle, bdev->endpoint_out); } } } } break; } } libusb_free_config_descriptor(conf_desc); return m_usbdev.bulk_interface; }4.2 控制传输int CLibusbControl::control_write(uint8_t bRequest, void *data, int len, int ms) { libusb_device_handle* handle = m_usbdev.handle; int status = libusb_control_transfer( handle, /* bmRequestType */ LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_INTERFACE, /* bRequest */ bRequest, /* wValue */ 0, /* wIndex */ 0, /* Data */ (unsigned char *)data, /* wLength */ len, ms); return status; } int CLibusbControl::control_read(uint8_t bRequest, void **data, int len, int ms) { libusb_device_handle* handle = m_usbdev.handle; int status = libusb_control_transfer( handle, /* bmRequestType */ LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_INTERFACE, /* bRequest */ bRequest, /* wValue */ 0, /* wIndex */ 0, /* Data */ (unsigned char*)(*data), /* wLength */ len, ms); return status; }4.3 批量传输批量读/** * @brief bulkusb_read * @param dev * @param buffer * @param len * @param ms * @return */ int CLibusbControl::bulkusb_read(void* buffer, int len, int ms) { int size, errcode; libusb_device_handle* handle = m_usbdev.handle; uint8_t endpoint_in = m_usbdev.endpoint_in; libusb_claim_interface(handle, m_usbdev.bulk_interface); errcode = libusb_bulk_transfer(handle, endpoint_in,(unsigned char*) buffer, len, &size, ms); if (errcode < 0) { printf("read: %s\n", libusb_strerror((enum libusb_error)errcode)); return -1; } int offset = 0, rsize = 0; if (size != len) { offset = len - size; errcode = libusb_bulk_transfer(handle, endpoint_in,(unsigned char*) buffer + offset, offset, &rsize, ms); if (errcode < 0) { printf("read: %s\n", libusb_strerror((enum libusb_error)errcode)); return -1; } } libusb_release_interface(handle, m_usbdev.bulk_interface); return size + offset; }批量写/** * @brief bulkusb_write * @param dev * @param buffer * @param len * @param ms * @return */ int CLibusbControl::bulkusb_write(void* buffer, int len, int ms) { int size, errcode; libusb_device_handle* handle = m_usbdev.handle; uint8_t endpoint_out = m_usbdev.endpoint_out; libusb_claim_interface(handle, m_usbdev.bulk_interface); errcode = libusb_bulk_transfer(handle, endpoint_out, (unsigned char*)buffer, len, &size, ms); if (errcode<0) { printf("write: %s\n", libusb_strerror((enum libusb_error)errcode)); return -1; } int offset = 0, rsize = 0; if (size != len) { offset = len - size; errcode = libusb_bulk_transfer(handle, endpoint_out,(unsigned char*) buffer + offset, offset, &rsize, ms); if (errcode < 0) { printf("write: %s\n", libusb_strerror((enum libusb_error)errcode)); return -1; } } libusb_release_interface(handle, m_usbdev.bulk_interface); return size+offset; }注意读写之前声明一下接口:libusb_claim_interface/libusb_release_interface 读写要校验数据是否读完,特别是当一次读取数据量非常大,并且最后一包小于usb 一帧packsize 时容易少接数据。5. 总结本文介绍了 libusb 以及使用方式,对于想学习usb host 开发的可以深入读一下libusb 源码学习一下。参考:https://libusb.sourceforge.io/api-1.0/ demo:git@github.com:Vinson-001/libusbsample.git
在进行 usb 传输的过程中,往往需要大容量传输。无外乎批量传输。典型的批量传输协议如下:cdc-acm :usb 转串口。win10 以下需要 pc 驱动。 mass-stroage:U 盘设备。通常仅仅只需要批量传输,而不想引入复杂的协议,这个时候自定义接口进行批量传输就比较合适了,需要注意的是自定义接口需要安装pc驱动, host 并不支持自定义的接口。描述符布局设备描述符:bDeviceClass 改为0xFF配置描述符:常规配置接口描述符:bInterfaceClass 改为 0xFFInerface Descriptor Endpoint Descruptor(in) Endpoint Descruptor(out)描述符介绍这里只介绍接口描述符和端点描述符,并且是独立的接口设备。自定义接口:关联两个端点static struct usb_interface_descriptor intf_desc = { .bLength = sizeof(intf_desc), .bDescriptorType = USB_DT_INTERFACE, .bNumEndpoints = 0x02, .bInterfaceClass = 0xFF, .bInterfaceSubClass = 0x0, .bInterfaceProtocol = 0x0, };端点描述符:in/out 端点static struct usb_endpoint_descriptor hs_bulk_in_desc = { .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_DIR_IN, .bmAttributes = USB_ENDPOINT_XFER_BULK, .wMaxPacketSize = cpu_to_le16(512), }; static struct usb_endpoint_descriptor hs_bulk_out_desc = { .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_DIR_OUT, .bmAttributes = USB_ENDPOINT_XFER_BULK, .wMaxPacketSize = cpu_to_le16(512), };整体接口描述符:static struct usb_descriptor_header *hs_intf_descs[] = { //(struct usb_descriptor_header *) &iad_desc, (struct usb_descriptor_header *) &intf_desc, (struct usb_descriptor_header *) &hs_bulk_out_desc, (struct usb_descriptor_header *) &hs_bulk_in_desc, NULL, };接口描述符如下:复合设备有的时候我们需要在已有的设备功能上,添加一个接口自定义功能。这个时候就需要添加复合设备。复合设备独立接口设备唯一区别在于接口描述符和设备描述符不同。设备描述符:按照杂项设备处理自定义接口,添加一个iad 关联IAD Descriptor Inerface Descriptor Endpoint Descruptor(in) Endpoint Descruptor(out)IAD 描述符:static struct usb_interface_assoc_descriptor iad_desc = { .bLength = sizeof(iad_desc), .bDescriptorType = USB_DT_INTERFACE_ASSOCIATION, .bFirstInterface = 0, // 动态修改 .bInterfaceCount = 1, .bFunctionClass = 0xFF, // vendor .bFunctionSubClass = 0x00, .bFunctionProtocol = 0x00, .iFunction = 0, };整体接口描述符:static struct usb_descriptor_header *hs_intf_descs[] = { (struct usb_descriptor_header *) &iad_desc, (struct usb_descriptor_header *) &intf_desc, (struct usb_descriptor_header *) &hs_bulk_out_desc, (struct usb_descriptor_header *) &hs_bulk_in_desc, NULL, };自定义接口描述符如下:驱动适配对于自定义接口,需要按照pc 驱动,否则 windows 端无法识别。使用 zadig-2.5.exe 按照对应接口的设备驱动。这里是接口2不同的驱动对应不同的系统调用。 winUSB:对应 libusb(1.0) 系统调用libusbK:对应libusbK 系统调用libusb-win32:对应libusb-0.1(libusb-win32) 系统调用有关libusb、libusbK、libusb-win32 之间的关系本文不做过多介绍,这里只是版本的差异。主机应用以libusb 为例介绍。控制传输int LIBUSB_CALL libusb_control_transfer(libusb_device_handle *dev_handle, uint8_t request_type, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout);控制写:... libusb_device_handle* handle = dev->handle; int status = libusb_control_transfer( handle, /* bmRequestType */ LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_INTERFACE, /* bRequest */ bRequest, /* wValue */ 0, /* wIndex */ 0, /* Data */ (unsigned char *)data, /* wLength */ len, ms);控制读:... libusb_device_handle* handle = dev->handle; int status = libusb_control_transfer( handle, /* bmRequestType */ LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_INTERFACE, /* bRequest */ bRequest, /* wValue */ 0, /* wIndex */ 0, /* Data */ (unsigned char*)(*data), /* wLength */ len, ms); return status;批量传输int LIBUSB_CALL libusb_bulk_transfer(libusb_device_handle *dev_handle, unsigned char endpoint, unsigned char *data, int length, int *actual_length, unsigned int timeout);批量读:int bulk_read(struct bulkusbdev* dev, void* buffer, size_t len, int ms) { int size, errcode; libusb_device_handle* handle = dev->handle; uint8_t endpoint_in = dev->endpoint_in; errcode = libusb_bulk_transfer(handle, endpoint_in,(unsigned char*) buffer, len, &size, ms); if (errcode < 0) { printf("read: %s\n", libusb_strerror((enum libusb_error)errcode)); return -1; } return size; }批量写:int bulk_write(struct bulkusbdev* dev, void* buffer, int len, int ms) { int size, errcode; libusb_device_handle* handle = dev->handle; uint8_t endpoint_out = dev->endpoint_out; errcode = libusb_bulk_transfer(handle, endpoint_out, (unsigned char*)buffer, len, &size, ms); if (errcode<0) { printf("write: %s\n", libusb_strerror((enum libusb_error)errcode)); return -1; } return size; }总结本文简单介绍了,USB 设备自定义批量传输的设备描述符实现,有关不同的平台,设备驱动实现方式不一。掌握了描述符,基本就掌握了设备实现的核心。参考对应平台做相关移植即可。
1. menuconfig 和Kconfig 介绍有过 linux 内核开发经验的人,对menuconfig 不会陌生。对于各类内核,只要是支持menuconfig 配置界面,都是使用Kconfig。换言之:menuconfig:支持配置内核的图形化界面。Kconfig:生成menuconfig 界面的脚本语言。故我们只要熟悉了Kconfig 的语法规则,就熟悉了 menuconfig的应用。这样对于基于menconfig 配置界面的各类内核都能得心应手了。2. 配置界面linux 内核的配置界面如下:思考两个问题:这个配置文件是如何组织和生成的?各项配置又是如何被使用的?2.1 界面生成和组织熟悉内核的都知道, 配置内核的过程中只需要执行 make menconfig 界面就自动出来了。那么,在执行make mneconfig 的过程中到底发生了什么?Makefile scripts/kconfig/Makefile顶层Makefile:依赖子makefile。这里MAKECMDGOALS 变量很重要。scripts/kconfig/Makefile。顶层makefile 执行make menuconfig 过程:首先执行顶层Makefile。然后执行scripts/kconfig/Makefile编译 kconfig 下相关工具。生成mconf 工具lxdialog:checklist/inputbox/menubox/texbox/yesno等图形显示组件mconf:menuconfig 读写解析应用 menu:菜单处理 conf:xxxdefconfig 解析mconf 核心代码如下://script/kconfig/mconf.c int main(int ac, char **av) { char *mode; int res; setlocale(LC_ALL, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); signal(SIGINT, sig_handler); conf_parse(av[1]); conf_read(NULL); mode = getenv("MENUCONFIG_MODE"); if (mode) { if (!strcasecmp(mode, "single_menu")) single_menu_mode = 1; } if (init_dialog(NULL)) { fprintf(stderr, N_("Your display is too small to run Menuconfig!\n")); fprintf(stderr, N_("It must be at least 19 lines by 80 columns.\n")); return 1; } set_config_filename(conf_get_configname()); conf_set_message_callback(conf_message_callback); do { conf(&rootmenu, NULL); res = handle_exit(); } while (res == KEY_ESC); return res; }我们可以看到,干了三件事:解析Kconfig:./mconf Kconfig生成对话框:init_dialog配置界面:conf最终会将配置界面设置保存成 .config 文件。2.2 .config 应用我们在编译内核前通过有两种操作:make xxx_defconfig;make uImage;make xxx_defconfig; make menucnfig; make uImage;make xxx_defconfig 操作会生成 conf 应用,并且解析xxx_defconfig。make menuconfig 操作会生成 mconf 应用,会将我们配置的选项保存成 .config当我们在执行编译内核的时候,会自动将 .config 里面的配置 转换成相应的宏到 autoconf.h 里面。2.2.1 .config 保存//mconf // 对话框退出 main() handle_exit() conf_write() //script/kconf/confdata.c conf_message(_("configuration written to %s"), newname) // 配置后保存 main() conf() conf_save() conf_write() //script/kconf/confdata.c conf_message(_("configuration written to %s"), newname) // conf : conf 直接将xxx_defconfig 保存成 .config main() conf_write()无论是是 menuconfg 配置, 还是解析 xxx_defconfig 最终都会先保存成 .config2.2.2 .config 解析当我们配置生成好 .config 后。在编译内核的时候,会提前将 .config 转换对应的宏。//conf.c main(); conf_write_autoconf(); file_write_dep("include/config/auto.conf.cmd"); .... if (!name) name = "include/generated/autoconf.h"; if (rename(".tmpconfig.h", name)) return 1; name = getenv("KCONFIG_TRISTATE"); if (!name) name = "include/config/tristate.conf"; if (rename(".tmpconfig_tristate", name)) return 1; name = conf_get_autoconfig_name();2.2.3 总结linux 内核主要是基于 Kconfig 来组织menuconfig从而达到配置内核的目的。那么主要从两方面入手。make menuconfig :生成mconf 应用,产生图形界面,并且保存.configmake xxx_defconf:生成conf 应用,解析xxx_defconf,并保存成.config本质上.config 和 xxx_defconfig 是同一文件。在编译内核的过程中会将 .config 转换为相应的宏,包含在头文件autoconf.h 中,这样就可以供内核使用了。故我们只需要掌握了 Kconfig 配置,就基本掌握了menuconfig 配置和添加流程。3. Kconfig 常用语法3.1 配置项前缀在Kconfig文件中,假设配置项的名字是XXX,在.config文件中:默认情况下,它对应的变量名为CONFIG_XXX如果设置了环境变量CONFIG_=ABC,则对应的变量名为ABC_XXX 3.2 单个配置项 config如图 USB Webcam Gadget 就是一个配置项:输入M:编成模块形式。.config 对应 CONFIG_USB_G_WEBCAM=M输入N:不选择配置。.config 对应 CONFIG_USB_G_WEBCAM is not set3.2.1. 语法我们还是以一个例子说明:# put drivers that need isochronous transfer support (for audio # or video class gadget drivers), or specific hardware, here. config USB_G_WEBCAM tristate "USB Webcam Gadget" depends on VIDEO_DEV select USB_LIBCOMPOSITE select VIDEOBUF2_VMALLOC help The Webcam Gadget acts as a composite USB Audio and Video Class device. It provides a userspace API to process UVC control requests and stream video data to the host. Say "y" to link the driver statically, or "m" to build a dynamically linked module called "g_webcam".config 配置基础语法如下:config option type "xxx" //简单描述 depends on xxx //依赖选项,可选 default xxx //初始值 help //帮助信息 xxxxxxxxxxxxxxxconfig option 是一个配置的开始,紧跟着的是配置选项的名称。config 下定义了以下属性:类型、输入提示、依赖关系、默认值、帮助信息。每个表示配置的菜单都有类型。变量共有五种类型。bool:布尔类型tristate:三态类型string:字符串hex:十六进制int:整型"USB Webcam Gadget":提示信息depends on:表示依赖关系。只有VIDEO_DEV 选中,才可以选择该项default:表示配置项的默认值。bool 类型可以是 y/nhelp:帮助信息3.2.2. 示例config TEST_A bool "config test A" default y help test config test A, yes/no config TEST_B bool "config test B" depends on TEST_A default y help test config test B, yes/noTEST_B 只有在A 选中才能生效。3.3. 菜单menu/endmenu如图 USB audio choice 包含两个配置项。UAC1_PLAY 和 UAC1_CAP3.3.1. 语法menu "USB audio choice" depends on GADGET_UAC1 config GADGET_UAC1_PLAY tristate "USB audio play" default y config GADGET_UAC1_CAP default y tristate "USB audio cap" choice prompt "cap from" default MIC depends on GADGET_UAC1_CAP config GADGET_UAC1_CAP_USER bool "cap from user" config GADGET_UAC1_CAP_MIC bool "cap from mic" endchoice endmenu我们先忽略 choice 和 endchoice。menu 语法以 menu 开始,endmenu 结束。中间包含若干项config配置, 当然也可以包含 其他语法。3.3.2. 示例menu "test menu" config TEST_MENU_A tristate "menu test A" config TEST_MENU_B bool "menu test B" default n endmenutest menu 包含两个配置项:3.4. 单选 choice/endchoice如图为 linux usb gadget 驱动,支持各种gadget 设备:3.4.1 语法choice config USB_G_HID tristate "HID Gadget" select USB_LIBCOMPOSITE help ¦ The HID gadget driver provides generic emulation of USB ¦ Human Inter # put drivers that need isochronous transfer support (for audio # or video class gadget drivers), or specific hardware, here. ...... config USB_G_WEBCAM tristate "USB Webcam Gadget" depends on VIDEO_DEV select USB_LIBCOMPOSITE select VIDEOBUF2_VMALLOC help ¦ The Webcam Gadget acts as a composite USB Audio and Video Class ¦ device. It provides a userspace API to process UVC control requests ¦ and stream video data to the host. ¦ Say "y" to link the driver statically, or "m" to build a ¦ dynamically linked module called "g_webcam". endchoicechoice 开始,endchoice 结束。choice 中间可以加入其他配置,以及 choice 嵌套他们之间只能有一个被设置为 y:表示编译进内核他们之间可以有多个被设置为 m:表示编译成模块3.4.2. 示例choice prompt "The maximal size of fifo" default USB_FIFO_512 config USB_FIFO_512 bool "512" config USB_FIFO_1024 bool "1024" config USB_FIFO_3072 bool "3072" endchoice 3.5. menuconfigmenuconfig XXX和config XXX类似, 唯一不同的是该选项除了能设置y/m/n外,还可以实现菜单效果(能回车进入该项内部)。比如 usb gadget 驱动就是 menuconfig 配置3.5.1. 语法menuconfig常用格式有2种:menuconfig M if M config C1 config C2 endif或者menuconfig M config C1 depends on M config C2 depends on M第1项menuconfig M跟config M语法是一样的, 不同之处在于menuocnfig M后面可以跟着好几个依赖于M的config C1、config C2等子配置项.3.6. if/endif3.6.1. 语法在上面的menuconfig中就有if/endif的使用,它的语法如下:if xxx endif3.6.2. 示例if USB_GADGET config USB_GADGET_DEBUG boolean "Debugging messages (DEVELOPMENT)" depends on DEBUG_KERNEL help ¦ Many controller and gadget drivers will print some debugging ¦ messages if you use this option to ask for those messages. ¦ A ...... endif只有定义了 USB_GADGET ,以下部分才会显示出来。3.7. source3.7.1. 语法source 语句用于读取另一个文件中的 Kconfig 文件, 比如driver/usb/Kconfig 中就包含了其他Kconfig:source "drivers/usb/gadget/Kconfig"3.8. comment3.8.1. 语法comment 语句出现在界面的第一行,用于定义一些提示信息,如:界面如下:4. 总结本文主要分析了 menuconfig 生成和使用过程,并且介绍了Kconfig 常见用法及语法介绍。有关 config/menu/menuconfig/choice等 Kconfig语法本质上就是脚本语言,也存在对应的相互组合关系,每种语法规则不是独立的。熟悉了 Kconfig 基本组织规则,那么就熟悉了 内核配置过程。无论是 linux 还是 rtos 只要是基于menconfig 配置的都是一通百通。
1.什么是libuvc ?libuvc 是一个基于 host 端的跨平台的 uvc 开源库,小巧简洁。目前支持h264/mjpeg/yuv完整的 uvc 协议支持(控制/流)扩展单元协议自定义。libuvc 是一个基于libusb 之上的协议,对于想学uvc的朋友,可以快速入手其源码。本文对其源码,不做过多分析,只对其环境构建做详细介绍。2.安装准备cmakegitgccpkg-configlibusblibuvc默认cmake/git/gcc/pkg-config 已安装,本文只对libusb 和libuvc 进行介绍。3.安装 libusblibusb 可以通过命令行安装,本文将从源码编译安装,方便大家后续在编的平台交叉编译。3.1下载git clone https://github.com/libusb/libusb.git3.2 编译安装第三方库sudo apt-get install autoconf libtool libudev-dev执行自动配置脚本,执行完成后会生成configure, INSTALL./autogen.sh配置configure(工具链, 安装路径,编译选项等) 见 INSTALLPWD=`pwd` rm -r $PWD/out mkdir $PWD/out PREFIX_DIR=$PWD/out # 这里只配置了 安装路径 ./configure --prefix=$PREFIX_DIR # 编译及安装 make && make install3 .编译成功如下:4. libuvc4.1 下载源码git clone https://github.com/libuvc/libuvc.git4.2 编译打开README。可以看到安装编译目录指定第三方库路径1.1 libusb# 设置环境变量,并将libusb-1.0.pc 复制到改路径下 export PKG_CONFIG_PATH=/home/wxyang/work/github/usb/3rd/pkgconfig # 查找库(测试) pkg-config --libs libusb-1.0 # 查找头文件(测试) pkg-config --cflags libusb-1.01.2 opencv 由于opecv 依赖的三方库太多,故不从源码介绍安装,直接通过apt-get 安装sudo apt-get install libopencv-dev python-opencv # 测试 pkg-config --cflags opencv1.3 libjpeg 可选,如果要支持mjpeg 编码需要安装,也可通过ffmpeg解码编译安装#!/bin/bash PWD=`pwd` rm $PWD/build mkdir $PWD/build mkdir $PWD/out BUILD_DIR=$PWD/build PREFIX_DIR=$PWD/out cd BUILD_DIR # 指定安装路径 cmake -DCMAKE_INSTALL_PREFIX=$PREFIX_DIR -DBUILD_TEST=ON -DBUILD_EXAMPLE=ON .. # 这里只配置了 安装路径 make && make install ~5. 测试修改 makefile将编好的libuvc库 放到指定的位置并设置好环境变量 PKG_CONFIG_PATH3rd/pkgconfig 下放编译好的.pc 便于pkg-config 能找到libuvc.pc 里面指定了真正的库文件目录testuvc 里面是测试simple ,直接从libuvc里面拷贝一个例子注意使用pkg-congfig 默认是动态链接,如果想要镜头链接,将指定库文件里面的动态库全部删掉,就会自动静态链接。6. 总结本文记录一下在构建libuvc 的时候,所遇到的一些坑.整个过程下来,如果一个人折腾还是挺花时间。同时回顾一下第三库的构建过程,方便读者快速在其他平台编译。
1. 前言最近一段时间一直在研究windows 驱动开发,简单聊聊。对比 linux,windows 驱动无论是市面上的书籍,视频还是社区,博文以及号主,写的人很少,导致学习曲线直线上升。windows 驱动 从业 人员就更少了。开发环境部署麻烦。驱动安装发布麻烦,需要数字签名。如果是发布到windows update 库里面,还需要做微标认证。为什么还要写?因为在学习的过程中,发现很多东西还是很相同的,如果你是从事linux 开发,可能会有些启发,如果是对windows 驱动开发有需求,可能提供一些不成熟的建议。接下来开始正文,简单介绍下windows 驱动2. windows 体系架构2.1 操作系统与应用程序在许多现代操作系统中,应用程序和操作系统是相互隔离的。操作系统的核心代码运行在特权模式下,即内核模式。而应用程序运行在非特权模式下,即用户模式。操作系统和应用程序的关心类似于服务器和客户端的关系,这点在windows 平台下显得更加突出:几个概念:system 进程:windows 操作系统本身会起一个 system 进程(加载kernel32.dll),有点类似于 linux 下的 init进程,具体细节不展开。FDO (Function Driver Object):设备功能驱动FiDO (Filter Driver Object):过滤驱动PDO (Physical Driver Object):物理设备驱动,真正访问硬件的地方。IRP (I/O Request Packet),应用程序 想要访问内核数据,必须通过IRP 传递。又叫IRP请求,当应用程序和驱动交互时,发送一个IRP 请求,IRP 会在各层设备驱动之间来回传动与转发。2.2 操作系统分层windows 的设计思想是将内核设计的尽可能的小,并且采用“客户端-服务器”的结构。操作系统各个组件或者模块是通过消息进行通信的。win32 子系统:是最纯正的windows 系统,其他子系统都是通过win32 子系统的接口来实现的,一般很少用到。Natvie API :在win32 api 基础上加上Nt 前缀,基于版本兼容考虑系统服务:Native API 从用户模式进入内核模式,调用系统服务。(软中断方式实现,陷入内核)执行组件:内核模式下的一组服务函数。对象管理程序:windows 操作系统提供的服务几乎都是以对象的形式存在的,这里的对象类似于面向对象语言中对象的概论。如驱动对象,设备对象等管理。进程管理程序:负责创建和终止进程,线程调度是由内核负责的。进程管理程序依赖于其他执行组件。虚拟内存管理程序:在CPU的内存管理单元(MMU)的协助下,通过某种映射将物理内存和虚拟内存关联起来。I/O 管理器:负责发起I/O 请求,并管理请求。它由一系列内核模式下的例程所组成,这些例程为用户模式下的进程提供了统一接口。I/O 管理器的目标是使来自用户模式的I/O请求独立于设备。配置管理程序:配置管理程序,记录所有计算机软,硬件的配置信息。它使用一个被称为注册表的数据库保存这些数据。设备驱动程序根据注册表中的信息进行加载驱动程序:I/O 管理器接收应用程序后,创建相应的 IRP,并传送至驱动程序进行处理:根据IRP的请求,直接操作硬件,然后完成此IRP,并返回根据IRP的请求,转发到更底层的驱动中去,并等待底层驱动的返回。接受到IRP驱动后,不着急于完成。而是分配新的IRP法定其他驱动程序,并等待返回。内核:内核被认为是 Windows 操作系统的心脏。Windows 的内核从执行组件分割出来。和执行组件相比,内核是非常小的:对内核对象的支持。对线程的调度对多处理器同步支持。中断处理函数的支持。对异常陷阱的支持。对其他硬件特殊功能的支持。硬件抽象层:不同的硬件平台,提供不同的硬件抽象层,并对上层提供统一的操作硬件的接口。2.3 应用程序和驱动3. windows 驱动框3.1 驱动模型:windows 驱动大致分为这几类:function driver:设备功能驱动filter driver:设备辅助驱动software driver:软件模块驱动bus driver:总线设备驱动3.2 驱动演变NT 模型 :2000以前,不支持 PNP (即插即用设备)WDM 模型:NT 基础上,支持 PNPWDF:WDM的重封装KMDF:内核模式(sys)UMDF:用户模式(dll)win7 x64 划时代操作系统,开始数字签名了win10 :双认证签名,也就是说对驱动的安全性要求更高了3.3 驱动垂直层次结构设备的创建顺序,先创建底层PDO,在创建高层的FDO,即从底层设备到高层设备。在PDO 和 FDO之间可能有各种过滤驱动。每层设备对象由不同的驱动程序创建,或者说每层的设备对应着不同的驱动程序。底层设备对象寻找上一层的设备对象,是依靠底层设备对象的AttachedDevice 来寻找的。3.4 驱动水平层次结构同一驱动程序创建出来的设备对象的关系称之为水平层次关系。每一个设备通过NextDevice可以寻找水平层次的下一个设备对象。3.5 一个复杂的驱动结构4. windows 开发环境搭建4.1 开发环境部署以 win10 为例,列出需要安装的东西,详细过程限于篇幅以后更新安装VS2019安装 Windows SDK:VS 2019 顺带安装安装 WDK安装 VMware + Win10 虚拟系统4.2 常用调试工具windbg:调试内核。WDK 自带,配合串口或者网络调试windows 内核DebugViewer:查看内核打印driverMonitor:驱动安装PCHunter_free:驱动强制卸载,不支持2004版本devicetree:设备枚举winobj:查看符号链接5. windows 驱动学习建议有关Windows 驱动开发书籍,博文,教程甚少。如果有这方面需求的可以给以下几个建议:环境搭建:win7 32 + vs2013 即可。不建议 win10 + vs2019,比较新,遇到问题不好解决。其次早期的一些调试工具,win10 最新版本不一定支持。绕开数字签名:先使用测试模式,安装驱动。否则光安装部署就够折腾的。入门采用 WDM 驱动模型。市面上将WDM 的书籍和资料相对多些,相对来说,WDF 开发资料来不少。多看书,windows 驱动不想linux 资料一大把,遇到不懂得,加技术群,啃书本。推荐书籍:《Windows 驱动开发技术详解》:已绝版《竹林蹊径:深入理解windows 驱动开发》:已绝版《windows 7:设备驱动程序开发 》:wdf 讲的比较多得书
背景对于静态库的封装,大多数情况在应用层应用的封装的比较多,用起来比较熟悉。不过,在嵌入式开发中,有些时候,需要将一些私有修改隐藏起来,特别是,内核中的一些修改。此时需要在内核态制作静态库,然后链接到整个内核文件中。对于一般(没有复杂的内核依赖关系)的内核静态库的封装,直接安装应用层封装即可。对于内核中一些高级驱动的私有修改,在进行封装时,就需要格外注意了,包括正确编译,头文件交叉引用,如果正确被链接到内核中,而不是被编译器忽略掉了。封装问题我们以 usb_f_uvc.ko 这个uvc function driver为例,来分析,内核静态库的封装(假设,以下文件有修改或者定制)。最终,将usb_f_uvc.ko 打包成一个 静态库,链接到内核里面。# kernel/drivers/usb/gadget/function/Makefile usb_f_uvc-y := f_uvc.o uvc_queue.o uvc_v4l2.o uvc_video.o uvc_configfs.o obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o编译我们将需要的文件,复杂到一个目录下,修改Makefile# Makefile # 可换成自己的工具链 CROSS_COMPILE ?= arm-linux-gnu- CC := $(CROSS_COMPILE)gcc LD := $(CROSS_COMPILE)ld AR := $(CROSS_COMPILE)ar CP := cp RM := rm # 修改正确的kernel 路径 KERNEL_PATH := xxxx/kerenl # 获取gcc 版本 CC_PATH := ${shell which $(CC)} CROSS_COMPILE_PATH := ${shell dirname $(CC_PATH)} CFLAGS := -nostdinc -isystem $(CROSS_COMPILE_PATH)/../lib/gcc/arm-linux-gnu/7.2.0/include # 头文件顺序很重要,换成自己平台的 INCLUDE = -I$(KERNEL_PATH)/arch/arm/include \ -I$(KERNEL_PATH)/arch/arm/include/generated/uapi \ -I$(KERNEL_PATH)/arch/arm/include/generated \ -I$(KERNEL_PATH)/include \ -I$(KERNEL_PATH)/arch/arm/include/uapi \ -I$(KERNEL_PATH)/include/uapi \ -I$(KERNEL_PATH)/include/generated/uapi/ \ -include $(KERNEL_PATH)/include/linux/kconfig.h INCLUDE += -I$(KERNEL_PATH)/arch/arm/xxxx/core/include \ -I$(KERNEL_PATH)/arch/arm/xxxx/soc-xxx/include \ -I$(KERNEL_PATH)/arch/arm/include/asm/mach-generic #CFLAGS += -fno-delete-null-pointer-checks -Wno-maybe-uninitialized -Wno-frame-address -Wno-format-truncation \ #-Wno-format-overflow -Wno-int-in-bool-context -Os --param=allow-store-data-races=0 -DCC_HAVE_ASM_GOTO \ #-Wframe-larger-than=1024 -fno-stack-protector -Wno-unused-but-set-variable -Wno-unused-const-variable \ #-fomit-frame-pointer -fno-var-tracking-assignments -Wdeclaration-after-statement \ #-Wno-pointer-sign -fno-strict-overflow -fconserve-stack -Werror=implicit-int \ #-Werror=strict-prototypes -Werror=date-time CFLAGS += -DEXPORT_SYMTAB # 这个一定要加 CFLAGS += -D__KERNEL__ CFLAGS += $(INCLUDE) OBJS := uvc_queue.o uvc_v4l2.o uvc_video.o f_uvc.o uvc_configfs.o ARFLAG := -rcs LIB_TARGET := libxxx.a TARGET := libxxx.hex all: $(TARGET) %.o:%.c $(CC) $(CFLAGS) -o $@ -c $^ $(TARGET): $(LIB_TARGET) $(CP) $(LIB_TARGET) $(TARGET) $(CP) -vf $(TARGET) $(KERNEL_PATH)/drivers/usb/gadget/function/ $(LIB_TARGET): $(OBJS) $(AR) $(ARFLAG) $@ $^ clean: find . -name "*.o" | xargs rm -r $(RM) -vf $(LIB_TARGET) $(TARGET) install: $(CP) -vf $(TARGET) $(KERNEL_PATH)/drivers/usb/gadget/function/Makefile 参数和头文件如何来?事实上,整个内核打包的过程,笔者认为,编译是最难的一步,特别是第一次接触的时候。对于驱动中的各符号和宏的定义,以及头文件包含是层层套娃,根据错误信息定位,简直要崩溃。在这里,笔者建议,先参考【内核编译参数选项】,然后在逐一删减无关选项,这样会方便很多。具体操作如下:正常编译内核:touch 修改 f_uvc.c:重新编译内核:make uImage V=1 > build.txtvim build.txt 搜索f_uvc 即可看到编译信息使用 make V=1 参数将编译的详细信息输出,包括头文件包含顺序,gcc 编译参数选项等,然后将其添加到我们的Makefie上。最后在对我们的Makfile 做删减。添加到内核#kernel/drivers/usb/gadget/function/Makefile usb_f_uvc-y := libxxx.a #obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o obj-y += usb_f_uvc.o # 防止Make distclean 把所有 .a都清掉了 $(obj)/libxxx.a: $(obj)/libxxx.hex cp $(obj)/libxxx.hex $(obj)/libxxx.a编译内核重新编译内核,将.a 链接到内核。然后烧到板子运行。运行实际运行,发现根本没有链到板子去。原因分析查看 EXPORT_SYMBOL打开 Module.symvers 发现,uvc 相关的接口并没有导出来,猜测有可能没有成功链到内核。vim Module.symversobjdump 反汇编使用objdump 将所有的符号表都输出来,然后在搜索查看,进一步确认链接是否正确。结果发现也找不到任何符号信息arm-linux-gnu-objdump -Dz vmlinux > kernel.dump此时一个大胆的想法出现了,是否是被编译器给优化掉了?因为是静态库,对于库文件来说,其本身只是一些接口,自身不能执行调用过程。如果接口没有人调用,那么所有相关的符号是否自动被忽略了?考虑一波对编译链接的理解分析源码//f_uvc.c DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Laurent Pinchart");这里的 DECLARE_USB_FUNCTION_INIT 很重要。我们,具体展开。#define DECLARE_USB_FUNCTION_INIT(_name, _inst_alloc, _func_alloc) \ DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc) \ static int __init _name ## mod_init(void) \ { \ return usb_function_register(&_name ## usb_func); \ } \ static void __exit _name ## mod_exit(void) \ { \ usb_function_unregister(&_name ## usb_func); \ } \ module_init(_name ## mod_init); \ module_exit(_name ## mod_exit)这里看到 module_init 应该很熟悉了,对于我们上面封装的库来说,本质上也是一个驱动,是驱动就有对应的入口和出口。对于内核,所有的入口都被放在 .text.init 处,加载到内核中后会按照相应顺序进行初始化。如果我们,把整个驱动封装成一个静态库,DECLARE_USB_FUNCTION_INIT 属于库的接口,本身不会自己调用。所以内核在链接的过程中,发现没有调用关系,就自然而然会忽略掉libxxx.a的相关符号。知道了原因,解决方法就很简单了。在内核中一定要存在有调用DECLARE_USB_FUNCTION_INIT的地方。方法1:手动调用。不推荐方法2:自动调用。沿用内核驱动模型。将 DECLARE_USB_FUNCTION_INIT 从静态库中剥离出来,其他文件打包成一个库。修改如下:// entry.c #include <linux/kernel.h> #include <linux/module.h> #include <linux/device.h> #include <linux/errno.h> #include <linux/list.h> #include <linux/mutex.h> #include <linux/string.h> #include <linux/usb/ch9.h> #include <linux/usb/gadget.h> #include <linux/usb/video.h> #include "u_uvc.h" #include "f_uvc.h" static struct usb_function_instance *uvc_alloc_inst(void) { return uvc_alloc_inst_callback(); } static struct usb_function *uvc_alloc(struct usb_function_instance *fi) { return uvc_alloc_callback(fi); } DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Laurent Pinchart");重新修改Makefileusb_f_uvc-y := entry.o libxxx.a obj-y += usb_f_uvc.o #obj-$(CONFIG_USB_F_UVC) += usb_f_uvc.o $(obj)/libxxx.a: $(obj)/libxxx.hex cp $(obj)/libxxx.hex $(obj)/libxxx.a这样重新,编译内核,就可以用了。以后只需要更新libxxx.a 即可。总结本文简单介绍内核静态库,打包遇到的一些坑。通过一个例子,介绍内核静态库的封装,以及遇到的问题。同时也加深了对编译和链接的理解。有关应用层静态库和内核态的库在使用上是一样的,不过在制作时有些许麻烦。头文件的引用包含编译参数选项是否成功链接有关驱动入口的部分,不能做到库里面,避免踩雷。折腾其他,结果发现是链接时出了问题。
1. 什么是 C 语言标准C 语言程序通过编译器,参考不同架构的指令集,编译生成对应的二进制指令,才能在不同的处理器上运行。在早期,各大编译器厂商在开发各自编译器时,各自开发,各自维护,比较混乱,造成一个局面:程序员写的程序,在一个编译器上可以通过,在另一个编译器上编译可能就通不过。为了统一标准,美国国家标准协会(American National Stardards Institude, ANSI)联合国际化标准组织(International Organization for Standardization, ISO)召集各个编译器厂商和各种技术团体一起开会,开始推行C语言的标准化行动。1989 年,发布了第一版C语言标准,并在第二年做了一些改进,因为在1989年发布的,所以一般称其为C89或者C90标准,或者叫做ANSI C 标准。2. C 语言标准内容C 语言标准,总体归纳起来,主要就是C语言编程的一些语法惯例、约定规则,如:定义了各种关键字、数据类型;定义各种运算规则、各种运算符的优先级和结合性;数据类型转换;变量的作用域;函数原型、函数嵌套层数、函数参数个数限制;标准库函数接口;C 语言标准发布后,大家都按照这个标准开展工作:程序员开发程序时,按照这种标准规定的语法规则编写程序;编译器厂商开发编译器工具时,也按照这种标准去解析。不同的编译器厂商支持统一的C语言标准,我们编写同一个程序使用不同的编译器都可以正常编译和运行。3. C 语言标准发展C 语言的标准并不是永远不变的,和其他标准一样,也是在不断发展的,C 语言标准经历了下面四个阶段。K&R C;ANSI C;C99;C11;K&R CK&R C 一般也成为传统C。在C语言标准没有统一之前,C 语言的作者 Dennis M.Ritchie 和 Brian W.Kernighan合作写了一本书《C程序设计语言》。早期程序员编程,这边书可以说绝对是权威的。这本书很薄,内容精炼,主要介绍了C语言的基本语法,后来第二版问世,做了一些修改。第二版可以看作ANSI标准的雏形,但早期的C语言还是很简单的,如还没有定义标准库函数,没有预处理命令等。ANSI CANSI C 是ANSI 在K&R C 的基础上,【统一了各大编译器厂商的不同的标准,并对C语言的语法和特性做了一些扩展】,在1989年发布的一个标准。这个标准一般也叫做C89/C90标准,也是目前各种编译器默认支持的C语言标准。ANSI C 标准主要新增了以下特性。增加了signed、volatile、const关键字增加了 void* 数据类型增加了预处理器命令增加了宽字符、宽字符串定义了C标准库......C99 标准C99 标准 是ANSI 在1999年基于C89标准发布的一个新标准。该标准对ANSI C 标准做了一些扩充,如新增了一些关键字,支持新的数据类型等。布尔型:_Bool复数:_Complex虚数:_Imaginary内联:inline指针修饰符:restrict支持 long long、long double 数据类型支持变长数组允许对结构体特定成员赋值支持十六进制浮点数、float _Complex等数据类型......C99 标准也会借鉴其他标准的优点,对自身的语法和标准做一系列修改:变量声明可以放在代码的任意位置。ANSI C 标准规定了变量的声明要全部写在函数语句的最前面,否则会编译报错。源程序每行最大支持4095个字节。支持 // 单行注释。早期的ANSI C标准使用 /**/ 注释,从C99标准开始也支持这种注释方式。标准库新增了一些头文件,如stdbool.h、complex.h、stdarg.h、fenv.h等。C11 标准C11 标准 是ANSI 在2011 年发布的最C语言标准,C11 标准修改了 C 语言标准的一些bug,增加了一些新特性。增加_Noreturn,声明函数无返回值。增加_Generic,支持泛型编程。修改了标准库函数的一些bug,比如gets()函数被gets_s()函数代替新增文件锁功能。......C17 标准 C17(也被称为为 C18)是于2018年6月发布的 ISO/IEC 9899:2018 的非正式名称,也是目前(截止到2020年6月)为止最新的 C语言编程标准,被用来替代 C11 标准。C17 没有引入新的语言特性,只对 C11 进行了补充和修正。4. 编译器对 C 语言的支持标准是一回事儿,编译器支不支持是另外一回事儿。不同的编译器对C语言标准支持不一样。有的编译器只支持 ANSI C 标准,这是目前默认的C语言标准。有的编译器可以支持C99,或者支持C99 标准的部分特性。对于 C11 和 C17 ,大多数编译器可能暂时还不支持。目前对于C99标准支持最好的就是GNU C 编译器了,据说可以支持C99标准99%的新特性。5. 编译器对 C语言标准的扩展不同编译器,出于开发环境、硬件平台、性能优化需要,除了支持C 语言标准,还会自己做一些扩展。比如GCC 编译器就对C语言做了需要扩展:零长度数组;语句表达式;内建函数;__attribute__ 特殊属性声明。标号元素case 范围......
在USB规范中,端点描述符用于描述设备的端点信息。UVC 端点描述符 分为两大类:VideoControl Endpoint Descriptor:视频控制接口端点描述符。VideoStream Endpoint Descriptor:视频流接口端点描述符。VC Endpoint Descriptor视频控制接口(VC)的端点描述符可以存在,也可以不存。默认使用端点0进行控制传输如果存在,必须是中断类型的,且必须为输入端点。Standard VC Interrupt Enpoint DescriptorbLength : 描述符大小.固定为0x07.bDescriptorType : 接口描述符类型.固定为0x05.bEndpointType : USB设备的端点地址.Bit7为方向位,必须为1,用于标识为输入端点;BIT4-6为0,BIT0-3表示端点ID。bmAttributes : 端点属性.BIT0-1必须为11,标识为中断端点;BIT2-3对于同步/等时传输有效,这里必须为00;wMaxPacketSize:本端点接收或发送的最大信息包大小.bInterval : 时间间隔,对于全速为1ms为单位;对于高速为2的N次方。static struct usb_endpoint_descriptor uvc_control_ep = { .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_DIR_IN, .bmAttributes = USB_ENDPOINT_XFER_INT, .wMaxPacketSize = cpu_to_le16(UVC_STATUS_MAX_PACKET_SIZE), .bInterval = 8, };Class-Specific Standard VC Interrupt Enpoint DescriptorbLength : 描述符大小.固定为0x05.bDescriptorType : 描述符类型.固定为USB_DT_CS_ENDPOINTbDescriptorSubType : 描述符子类。固定为UVC_EP_INTERRUPTwMaxTransferSize:最大传输sizestatic struct uvc_control_endpoint_descriptor uvc_control_cs_ep = { .bLength = UVC_DT_CONTROL_ENDPOINT_SIZE, .bDescriptorType = USB_DT_CS_ENDPOINT, .bDescriptorSubType = UVC_EP_INTERRUPT, .wMaxTransferSize = cpu_to_le16(UVC_STATUS_MAX_PACKET_SIZE), };VS Endpoint DescriptorVS 中的端点描述符用于传输视频数据。VS Data Endpoint 可以是同步传输(iso)或者批量传输(bulk)Standard VS Isochronous Video Data Endpoint DescriptorbLength : 描述符大小.固定为0x07.bDescriptorType : 接口描述符类型.固定为0x05.bEndpointType : USB设备的端点地址.Bit7为方向位,必须为1,用于标识为输入端点;BIT4-6为0,BIT0-3表示端点ID。bmAttributes : 端点属性.BIT0-1必须为01,标识为等时传输;BIT2-3:00:无同步01:异步(UVC规范上设置为异步)10:适配11:同步wMaxPacketSize:本端点接收或发送的最大信息包大小.bInterval : 时间间隔高速设备static struct usb_endpoint_descriptor uvc_hs_streaming_ep = { .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_DIR_IN, .bmAttributes = USB_ENDPOINT_SYNC_ASYNC | USB_ENDPOINT_XFER_ISOC, /* The wMaxPacketSize and bInterval values will be initialized from * module parameters. */ };Standard VS Bulk Video Data Endpoint Descriptor当为批量传输时,和普通的批量端点描述符一致。这里需要注意bmAttributes设置参考《UVC 1.5 Class specification.pdf》 《USB_Video_Example 1.5.pdf》 www.usbzh.com
1. 标准VS接口标准 VS 接口描述符和标准接口描述符的定义是一样的,只是用来表示接口本身。bLength:描述符长度,固定为9bDescriptorType:描述符类型。这里为接口描述符,设为0x4bInterfaceNumber:接口索引,0表示VideoControl,1表示VideoStreambAlternateSetting:可替换设置索引bNumEndpoints:端点0以外的端点数bInterfaceClass:接口类,0xE表示Video_ClassbInterfaceSubClass:接口子类,这里是0x1表示VideoControl,0x2表示VideoStreambInterfaceProtocol:协议代码,uvc1.5的协议所以须设为1.iInterface:接口字符串描述符的索引值static struct usb_interface_descriptor uvc_streaming_intf_alt0 = { .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, .bInterfaceNumber = UVC_INTF_VIDEO_STREAMING, .bAlternateSetting = 0, .bNumEndpoints = 0, .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = UVC_SC_VIDEOSTREAMING, .bInterfaceProtocol = 0x00, /*uvc1.5 设置为1 */ .iInterface = 0, }; static struct usb_interface_descriptor uvc_streaming_intf_alt1 = { .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, .bInterfaceNumber = UVC_INTF_VIDEO_STREAMING, .bAlternateSetting = 1, .bNumEndpoints = 1, .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = UVC_SC_VIDEOSTREAMING, .bInterfaceProtocol = 0x00, .iInterface = 0, };2. 特殊类描述符类特殊描述符包含Inputer Header 、Output Header, Format 和 Frame 描述符。即包含:输入头;输出头;视频格式;帧信息;每个VS 接口都有一个输入或者输出头描述符,一个支持的视频格式描述符,以及一个或多个帧描述符。也会是说每种视频格式都需要一个VS接口。每个VS 接口对应处理一种视频数据。2.1 Input 接口输入头描述符,包含输入端点,用来处理video stream 数据,同时提供了不同格式的描述符信息。bLength:描述符长度。bDescriptorType:描述符类型bDescriptorSubtype:描述符子类,输入头bNumFormats:支持的视频格式wTotalLength:整个 VS 接口描述符返回的总长度,包含头bEndpointAddress:端点地址bmInfo:默认设为0,支持视频格式动态修改bTerminalLink:OT 的Terminal IDbStillCaptueMethod:支持image capturebTriggerSupport:特殊的硬件触发bTriggerUsage:默认为0bControlSize:bmaControls numberDECLARE_UVC_INPUT_HEADER_DESCRIPTOR(1, 2); static const struct UVC_INPUT_HEADER_DESCRIPTOR(1, 2) uvc_input_header = { .bLength = UVC_DT_INPUT_HEADER_SIZE(1, 2), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VS_INPUT_HEADER, .bNumFormats = 2, .wTotalLength = 0, /* dynamic */ .bEndpointAddress = 0, /* dynamic */ .bmInfo = 0, .bTerminalLink = 3, .bStillCaptureMethod = 0, .bTriggerSupport = 0, .bTriggerUsage = 0, .bControlSize = 1, .bmaControls[0][0] = 0, .bmaControls[1][0] = 4, };2.2 Output Header输出头描述符,包含一个输出端点,用来处理video stream数据,同时提供不同的格式描述bLength:描述符长度。bDescriptorType:描述符类型bDescriptorSubtype:描述符子类,输出头bNumFormats:支持的视频格式wTotalLength:整个 VS 接口描述符返回的总长度,包含头bEndpointAddress:端点地址bTerminalLink:IT 的Terminal IDbStillCaptueMethod:支持image capturebControlSize:bmaControls number2.3 格式和帧描述Payload Format Descriptors视频格式描述符定义了,特定的格式信息,相关介绍记录在以下文档中:MJPEGbLength:描述符长度。bDescriptorType:描述符类型bDescriptorSubtype:描述符子类,MJPEG格式bFormatIndex:格式描述符索引bNumFrameDescriptors:帧描述符个数bmFlags:指定特征格式bDefaultFrameIndex:初始化帧索引,用来选择默认分辨率bAspectRatioX:x 方向图片尺寸比例bAspectRatioY:x 方向图片尺寸比例bmInterfaceFlags:bm 接口控制,默认未用bCopyProtect:复制保护,可限制重复。static const struct uvc_format_mjpeg uvc_format_mjpg = { .bLength = UVC_DT_FORMAT_MJPEG_SIZE, .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VS_FORMAT_MJPEG, .bFormatIndex = 2, .bNumFrameDescriptors = 2, /* /* 表示支持 两种分辨率格式 */*/ .bmFlags = 0, .bDefaultFrameIndex = 1, .bAspectRatioX = 0, .bAspectRatioY = 0, .bmInterfaceFlags = 0, .bCopyProtect = 0, };YUVUVC协议未压缩的格式目前支持以下几种:比MJPEG相比多了guidFormat这项bLength:描述符长度。bDescriptorType:描述符类型bDescriptorSubtype:描述符子类,未压缩格式bFormatIndex:格式描述符索引bNumFrameDescriptors:帧描述符个数guidFormat:指定格式bBitsPerPixel:每个像素占字节bDefaultFrameIndex:初始化帧索引,用来选择默认分辨率bAspectRatioX:x 方向图片尺寸比例bAspectRatioY:x 方向图片尺寸比例bmInterfaceFlags:bm 接口控制,默认未用bCopyProtect:复制保护,可限制重复。static const struct uvc_format_uncompressed uvc_format_yuv = { .bLength = UVC_DT_FORMAT_UNCOMPRESSED_SIZE, .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VS_FORMAT_UNCOMPRESSED, .bFormatIndex = 1, .bNumFrameDescriptors = 2, .guidFormat = { 'Y', 'U', 'Y', '2', 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}, .bBitsPerPixel = 16, .bDefaultFrameIndex = 1, .bAspectRatioX = 0, .bAspectRatioY = 0, .bmInterfaceFlags = 0, .bCopyProtect = 0, };帧描述符主要描述视频帧信息,包括分辨率大小,帧间隔等MJPEGbLength:描述符长度bDescriptorType:描述符类型,bDescriptorSubType:描述符子类bFrameIndex:帧索引。bmCapabilities:是否支持still imagewWidth:帧款,wHeight:帧高,dwMinBitRate:最小速率dwMaxBitRate:最大速率,dwMaxVideoFrameBufferSize:最大bufferdwDefaultFrameInterval:初始化帧间隔,支持的帧率。bFrameIntervalType:帧间隔类型。0,Continuous Frame Interval;1~255 discrete Frame Interval support(n)dwFrameInterval[n]:帧间隔设置,默认设置一个即可。static const struct UVC_FRAME_MJPEG(1) uvc_frame_mjpg_360p = { .bLength = UVC_DT_FRAME_MJPEG_SIZE(3), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VS_FRAME_MJPEG, .bFrameIndex = 1, .bmCapabilities = 0, .wWidth = cpu_to_le16(640), /* 分辨率 */ .wHeight = cpu_to_le16(360), .dwMinBitRate = cpu_to_le32(18432000), .dwMaxBitRate = cpu_to_le32(55296000), .dwMaxVideoFrameBufferSize = cpu_to_le32(460800), .dwDefaultFrameInterval = cpu_to_le32(666666), .bFrameIntervalType = 1, .dwFrameInterval[0] = cpu_to_le32(666666), };YUVYUV 帧描述符信息同MJPEGbLength:描述符长度bDescriptorType:描述符类型,bDescriptorSubType:描述符子类bFrameIndex:帧索引。bmCapabilities:是否支持still imagewWidth:帧款,wHeight:帧高,dwMinBitRate:最小速率dwMaxBitRate:最大速率,dwMaxVideoFrameBufferSize:最大bufferdwDefaultFrameInterval:初始化帧间隔,支持的帧率。bFrameIntervalType:帧间隔类型。0,Continuous Frame Interval;1~255 discrete Frame Interval support(n)dwFrameInterval[n]:帧间隔设置,默认设置一个即可。static const struct UVC_FRAME_UNCOMPRESSED(1) uvc_frame_yuv_360p = { .bLength = UVC_DT_FRAME_UNCOMPRESSED_SIZE(1), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VS_FRAME_UNCOMPRESSED, .bFrameIndex = 1, .bmCapabilities = 0, .wWidth = cpu_to_le16(640), .wHeight = cpu_to_le16(360), .dwMinBitRate = cpu_to_le32(18432000), .dwMaxBitRate = cpu_to_le32(55296000), .dwMaxVideoFrameBufferSize = cpu_to_le32(460800), .dwDefaultFrameInterval = cpu_to_le32(666666), .bFrameIntervalType = 1, .dwFrameInterval[0] = cpu_to_le32(666666), };3. 总结一个标准的uvc设备的VS 接口描述符如下,包括alt setting 0 和 alt setting1.alt setting0 用来配置视频格式和帧信息alt setting1 用来开启传输,通过In Endpoint 将video stream 传到pc.继续拆分,VS 接口描述符如下:VS InterfaceVS MJPEG FormatVS Uncompressed Foramt(YUY2/NV12等)...VS MJPEG FrameVS MJPEG FrameVS Uncompressed FrameVS Uncompressed FrameVideo Class-specific VS Input HeaderVS InterafaceIn Endpoint
1. 前言接上文,本文简化UVC拓扑结构,抽像出一个简单的VideoControl 来介绍常用的VC Interface 及接口描述符。如图所示,VC Interface 包含CT,PU、OT三部分,事实上只是一个接口,然后根据不同的子类型实现不同的单元(CT/PU/OT)每个Terminal或者Unit都有唯一的标识,各个单元之间的联系是通过bSourceID建立联系。2. VideoControl InterfaceVideoControl(以下简称VC)Interface 描述符,包含所有相关信息,以表示,相应的视频功能。VC 分类 Standard VC 和 Class-specific 接口,即标准VC接口和特殊类接口。2.1 标准VC接口标准 VC 接口描述符和标准接口描述符的定义是一样的,只是用来表示接口本身。bLength:描述符长度,固定为9bDescriptorType:描述符类型。这里为接口描述符,设为0x4bInterfaceNumber:接口索引,0表示VideoControl,1表示VideoStreambAlternateSetting:可替换设置索引bNumEndpoints:端点0以外的端点数,这里表示一个端点bInterfaceClass:接口类,0xE表示Video_ClassbInterfaceSubClass:接口子类,这里是0x1表示VideoControl,0x2表示VideoStreambInterfaceProtocol:协议代码,这里因为看的是uvc1.5的协议所以必须设为1.iInterface:接口字符串描述符的索引值关于PC_PROTOCOL_15 可参考以下连接说明https://patchwork.kernel.org/project/linux-media/patch/1447090438-28681-1-git-send-email-laurent.pinchart@ideasonboard.com/driver/usb/gadget/function/f_uvc.cstatic struct usb_interface_descriptor uvc_control_intf = { .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, .bInterfaceNumber = UVC_INTF_VIDEO_CONTROL, .bAlternateSetting = 0, .bNumEndpoints = 1, .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = UVC_SC_VIDEOCONTROL, .bInterfaceProtocol = 0x00, /* 默认不支持1.5协议 */ .iInterface = 0, };2.2 特殊类VC接口UVC 特殊类接口描述符是一连串的描述符,它包含了所有用来描述设备功能的描述符,比如 unit 和 terminal 描述符。它们直连的链接关系可以通过描述符中的 bSourceID 或者 baSourceID 。2.2.1 接口头类特殊描述符整个长度依赖于uint和terminal,因此该描述符以一个头为开始,如下图:bLength:描述符长度,为n+12bDescriptorType:描述符类型。bDescriptorType:0x1,表示是VC_HEADERbcdUVC:支持的UVC协议版本wTotalLength:整个类特殊描述符的长度dwClockFrequency:时钟频率bInCollection:指定设备拥有的 VideoStream 接口数baInterfaceNr(1):VideoStream 接口号baInterfaceNr(...):/* driver/usb/gadget/legacy/webcam.c */ DECLARE_UVC_HEADER_DESCRIPTOR(1); static const struct UVC_HEADER_DESCRIPTOR(1) uvc_control_header = { .bLength = UVC_DT_HEADER_SIZE(1), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VC_HEADER, .bcdUVC = cpu_to_le16(0x0150), .wTotalLength = 0, /* dynamic */ .dwClockFrequency = cpu_to_le32(48000000), .bInCollection = 0, /* dynamic */ .baInterfaceNr[0] = 0, /* dynamic */ };2.2.2 Camera Terminal DescriptorInput Terminal Descriptor (简称IT)。CT描述符,表示camera 输入描述符。其结构如下:bLength:描述符长度bDescriptorType:描述符类型,bDescriptorSubType:描述符子类,VC_INPUT_TERMINAL,bTerminalID:CT 唯一标识wTerminalType:Terminal typebAssocTerminal:用于将IT 和OT关联,如果不存在关联,设为0iTerminal:字符串描述符的索引,用来描述CTwObjectiveFocalLengthMin:默认设为0,不支持zoomwObjectiveFocalLengthMax:默认设为0wOcularFocalLength:默认设为0bControlSize:bm 控制bmControls[n]:具体控制支持/* driver/usb/gadget/legacy/webcam.c */ static const struct uvc_camera_terminal_descriptor uvc_camera_terminal = { .bLength = UVC_DT_CAMERA_TERMINAL_SIZE(3), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VC_INPUT_TERMINAL, .bTerminalID = 1, .wTerminalType = cpu_to_le16(0x0201), .bAssocTerminal = 0, .iTerminal = 0, .wObjectiveFocalLengthMin = cpu_to_le16(0), .wObjectiveFocalLengthMax = cpu_to_le16(0), .wOcularFocalLength = cpu_to_le16(0), .bControlSize = 3, .bmControls[0] = 2, .bmControls[1] = 0, .bmControls[2] = 0, };2.2.3 Processing Unit DescriptorProcessing Unit 描述符,以下简称PU 描述符,结构如下:bLength:描述符长度bDescriptorType:描述符类型,这里表示接口,bDescriptorSubType:描述符子类,这里表示一个PU单元bUnitID:PU 唯一标识bSourceID:源ID,wMaxMultiplier:bControlSize:bm控制占字节数bmControls[0]:bm 控制bmControls[1] :iProcessing:PU 字符串描述索引static const struct uvc_processing_unit_descriptor uvc_processing = { .bLength = UVC_DT_PROCESSING_UNIT_SIZE(2), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VC_PROCESSING_UNIT, .bUnitID = 2, .bSourceID = 1, .wMaxMultiplier = cpu_to_le16(16*1024), .bControlSize = 2, .bmControls[0] = 1, .bmControls[1] = 0, .iProcessing = 0, };2.2.4 Output Terminal DescriptorOutput Terminal Descriptor ,简称OT 描述符,结构如下:bLength:描述符长度bDescriptorType:描述符类型,表明这个一个接口bDescriptorSubTypeL:描述符子类,表示PU类型bTerminalID:OT 唯一标识wTerminalType:Terminal 类型,表明是一个Stream TerminalbAssocTerminal:无关联bSourceID:源ID,为2表示,OT 直接与PU相连(暂时忽略SU)iTerminal:未用。字符串描述索引。static const struct uvc_output_terminal_descriptor uvc_output_terminal = { .bLength = UVC_DT_OUTPUT_TERMINAL_SIZE, .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = UVC_VC_OUTPUT_TERMINAL, .bTerminalID = 3, .wTerminalType = cpu_to_le16(0x0101), .bAssocTerminal = 0, .bSourceID = 2, .iTerminal = 0, };2.2.5 其他其他接口单元暂时不做介绍,比如SU,和IT以及EU,XU等3. 总结本文以一个简单的模型介绍了,uvc的VideoContrl Interface 以及包含的描述符信息。通过此文,可以看出,VC Interface 事实上完成的就是两个功能:视频流输入输出:CT/OTCamera 控制:对焦、曝光、白平衡等等通过配置PU 和 CT 单元,可以为UVC设备添加各种各样的功能。同时,只有设备端描述符正确配置,主机才能获取并且知道设备到底支持哪些功能。
1. 概述描述符用来描述USB设备性能或特性的数据结构,与设备类相关的信息都是主机向设备获取描述符来得到的。一个UVC设备除了常用的标准描述符,另外还定义了视频设备的特殊类描述符,主要如下:标准描述符设备描述符(Device Descriptor)设备限定描述符(Device QualifierDescriptor)设备配置描述符(Configure Descriptor)其他速度描述符(Other Speed Descriptor)字符描述符(String Descriptor)特殊类描述符接口联合描述符(Interface Association Descriptor)视频控制接口描述符(VideoControl Interface Descriptor)视频控制端点描述符(VideoControl Endpoint Descriptor)视频流接口描述符(VideoStreaming Interface Descriptor)视频流端点描述符(VideoStreaming Endpoint Descriptor)接下来,我们将重点分析这些描述符信息。2. UVC设备逻辑组织uvc 设备逻辑组织如下。至少包含一个配置描述符两个接口,接口0负责VideoControl,接口1负责VideoStream。3. 描述符介绍3.1 设备描述符设备描述符信息如下(来源于USB_Video_Class_1.5)bLength:固定长度 0x12bDescriptorType:设备类型,默认设为1bcdUSB:USB版本号。1.1~0x0110 2.0~0x0200bDeviceClass:设备类.此处设为0xEFbDeviceSubClass:设备子类。此处设为0x02bDeviceProtocol:协议代码。默认为0x1bMaxPacketSize0:控制端点最大包长。动态设置idVendor:厂商编号idProduct:产品IDbcDevice:设备版本号iManufacturer:厂商字符串描述符索引iProduct:产品字符串描述符索引iSerialNumber:产品序列号描述符索引bNumConfigurations:支持的配置数量,一般为1对比 driver/usb/gadget/webcam.c,可以看到dynamic都是可以动态调整的。static struct usb_device_descriptor webcam_device_descriptor = { .bLength = USB_DT_DEVICE_SIZE, .bDescriptorType = USB_DT_DEVICE, .bcdUSB = cpu_to_le16(0x0200), .bDeviceClass = USB_CLASS_MISC, .bDeviceSubClass = 0x02, .bDeviceProtocol = 0x01, .bMaxPacketSize0 = 0, /* dynamic */ .idVendor = cpu_to_le16(WEBCAM_VENDOR_ID), .idProduct = cpu_to_le16(WEBCAM_PRODUCT_ID), .bcdDevice = cpu_to_le16(WEBCAM_DEVICE_BCD), .iManufacturer = 0, /* dynamic */ .iProduct = 0, /* dynamic */ .iSerialNumber = 0, /* dynamic */ .bNumConfigurations = 0, /* dynamic */ };3.2 配置描述符配置描述符如下:bLength:描述符长度,固定为0x9bDescriptorType:配置描述符类型。默认为0x2wTotalLength:表示整个配置描述符的总长度,包括配置描述符,接口描述符,类特殊描述符和端点描述符bNumInterFaces:配置支持的接口数bConfigurationValue:配置ID,每个配置都有一个标识值iConfiguration:配置描述符字符串索引。bmAttributes:描述供电特性 D7保留,D6辨识供电方式,为1表示自供电的,否则是总线供电,D5标识是否支持远程唤醒(1),D4-D0保留bMaxPower:总线供电时的最大电流,如值为100则最大电流为200mA。配置描述符配置在driver/usb/gadget/compsite.c下/* driver/usb/gadget/compsite.c */ static int config_buf(struct usb_configuration *config, enum usb_device_speed speed, void *buf, u8 type) { struct usb_config_descriptor *c = buf; void *next = buf + USB_DT_CONFIG_SIZE; int len; struct usb_function *f; int status; len = USB_COMP_EP0_BUFSIZ - USB_DT_CONFIG_SIZE; /* write the config descriptor */ c = buf; c->bLength = USB_DT_CONFIG_SIZE; c->bDescriptorType = type; /* wTotalLength is written later */ c->bNumInterfaces = config->next_interface_id; c->bConfigurationValue = config->bConfigurationValue; c->iConfiguration = config->iConfiguration; c->bmAttributes = USB_CONFIG_ATT_ONE | config->bmAttributes; c->bMaxPower = encode_bMaxPower(speed, config); ...... /* add each function's descriptors */ ...... len = next - buf; c->wTotalLength = cpu_to_le16(len); return len; }3.3 接口关联描述符(IAD)接口关联描述符信息如下:bLength:固定长度,为0x8bDescriptorType:接口关联描述符类型。0xb 表示IADbFristInterface:接口numberbInterfaceCount:连续的video 接口数,这里为2bFunctionClass:设备类。0xE 表示Video ClassbFunctionSubClass:子类。表示支持的具体功能。这里是0x3对应VIDEO_INTERFACE_COLLECTION,支持VideoControl 和VideoInterfacebFunctionProtocol:默认0iFunction:字符串索引可以看到,UVC设备通过 IAD 去描述一个视频接口集合(Video Interface Collection)注:Video Interface Collection 是一个视频接口集合。一个UVC设备必须通过IAD来描述Video Interface Collection,并且至少包含一个VideoControl Interface 和一个或多个VideoStream Interface/* driver/usb/gadget/function/f_uvc.c */ static struct usb_interface_assoc_descriptor uvc_iad = { .bLength = sizeof(uvc_iad), .bDescriptorType = USB_DT_INTERFACE_ASSOCIATION, .bFirstInterface = 0, .bInterfaceCount = 2, .bFunctionClass = USB_CLASS_VIDEO, .bFunctionSubClass = UVC_SC_VIDEO_INTERFACE_COLLECTION, .bFunctionProtocol = 0x00, .iFunction = 0, };4. 总结有关 UVC的描述符组织信息,实在太多,本文先介绍基础描述符,后续陆续介绍,VideoControl ,VideoStream等描述符信息,这个是UVC的关键。所有信息来源于《UVC 1.5 Class specfication》和《USB_Video_Example.pdf》以及内核源码参考。
1. Linux USB 子系统在介绍设备端驱动前,我们先来看看 Linux USB子系统。这里的子系统是相对于整个Linux kernel 来说的,而非单一设备。从整体概括了USB主机端和设备端的通信框架。Linux kernel 中早已集成了较为完善的USB协议栈,由于其规模庞大,包含多个类别的设备驱动,所以Linux系统中的USB协议栈也被称为USB子系统。1.1 主机端主机端,简化抽象三层:各种类设备驱动:mass sotrage, CDC, HID等USB 设备驱动:USB 核心处理主机控制器驱动:不同的USB主机控制器(OHCI/EHCI/UHCI),抽象为HDC。1.2 设备端设备端,也抽象为三层:设备功能驱动:mass sotage , CDC, HID 等,对应主机端的类设备驱动Gadget 设备驱动:中间层,向下直接和UDC通信,建立链接;向上提供通用接口,屏蔽USB请求以及传输细节。设备控制器驱动:UDC驱动,直接处理USB设备控制器。2. USB 设备驱动2.1 gadget 驱动框架拆解1我们将USB 设备端驱动拆解一下,其驱动框架如下:上文提到,Gadget 设备层起着至关重要的作用。为上层提供通用的驱动框架,与下层UDC通过Gadget Interface 建立联系。其中Compsite Framwork 提供了一个通用的usb_gadget_driver 模板,包括各种方法供上层Function driver 使用。(driver/usb/gadget/compsite.c)从上图我们可以看出,对于USB设备端驱动开发而言,更多的关注的是Function driver这层。USB 控制相关过程,内核提供了一个中间层帮我们屏蔽掉了。2.2 gadget 驱动框架拆解2内核版本:Linux Kernel 4.4.94,我们以这个版本进行拆解分析4.x 的内核相对于3.x的内核在gadget 驱动上分解的更加完善,显得目录结构,层次分明,分工合理,更便于理解。相对于3.x 的版本,4.4.94这个内核,将原来的、driver/usb/gadget目录进行拆分。通用接口保持不变,比如compsite.c以及functions.c。将usb function driver 进行细分,分为legacy和functions。有了这些背景,我们再看4.4.94这版内核,gadget驱动框架,如图所示(下图笔者按自己的理解绘制的):legacy:整个Gadget 设备驱动的入口。位于driver/usb/gadget/legacy下,里面给出了常用的usb类设备的驱动sample。其作用就是配置USB设备描述符信息,提供一个usb_composite_driver, 然后注册到composite层。functions:各种usb 子类设备功能驱动。位于driver/usb/gadget/functions,里面也给出了对应的sample。其作用是配置USB子类协议的接口描述以及其他子类协议,比如uvc协议,hid等。注意:对于一个compsite 设备一个有一个或者多个function,对应的也就有多个functions driver从这张图上,有没有发现,设备端驱动开发似乎越来越简单了。没错,事实上,我们只需要根据legacy的源码,添加对应的usb设备描述符信息,以及其他若干配置即可。换言之,我们只需要关心 legacy 这一丢丢就行,对于functions这层会根据业务需要略微调整,不过整体变动不大。usb 驱动框架之所以复杂,除了需要研究各种复杂的协议,还融合了各种驱动,对于初学者来说,理解起来有点困难。事实上,光是legacy这里也包含其他驱动,比如webcam里有大名鼎鼎的 v4l2 驱动框架。所以当我学习USB驱动框架的时候,一定要抓大放小,【把握主要脉络,忽略细节】。当我们把一个复杂的驱动逐一拆解的话,其实发现,就没有那么可怕了。2.3 usb compsite 设备构建为了便于理解,我们来简单了解一个usb compsite 设备的构建过程:假设构建一个usb 复合设备,需要支持uac, uac, hid 三个功能 其驱动框架如下:首先,我们需要一个驱动入口 legacy,用来配置设备描述信息,支持的协议等然后添加一个配置支持多种接口,这里支持uvc uac hid, 每个接口对应一个functions driver最后我们把它注册到compsite 层对于functions driver 有个usb function driver list,在内核注册function driver 时会自动添加到一个链表上。functions.c 就是用来管理所有的function drivers3. USB gadget 驱动剖析3.1 相关数据结构在梳理整个框架前我们先梳理一下几个重要的数据结构,从下到上依次介绍:usb_udc:udc 使用,内嵌usb_gadget_driver 和 usb_gadgetstruct usb_udc { struct usb_gadget_driver *driver; struct usb_gadget *gadget; struct device dev; struct list_head list; bool vbus; };usb gadget:usb 底层操作,包括udc,端点请求等。struct usb_gadget { struct work_struct work; /* 工作队列 */ struct usb_udc *udc; /* udc */ /* readonly to gadget driver */ const struct usb_gadget_ops *ops; /*gadget 设备操作函数集*/ struct usb_ep *ep0; /* 控制端点,只对setup包响应*/ struct list_head ep_list; /* 将设备的所有端点连成链表,ep0不在其中 */ enum usb_device_speed speed; /* 高速、全速和低速 */ enum usb_device_speed max_speed; /* 最大速度 */ enum usb_device_state state; const char *name; struct device dev; unsigned out_epnum; /* out ep number */ unsigned in_epnum; /* in ep number */ struct usb_otg_caps *otg_caps; unsigned sg_supported:1; unsigned is_otg:1; unsigned is_a_peripheral:1; unsigned b_hnp_enable:1; unsigned a_hnp_support:1; unsigned a_alt_hnp_support:1; unsigned quirk_ep_out_aligned_size:1; unsigned quirk_altset_not_supp:1; unsigned quirk_stall_not_supp:1; unsigned quirk_zlp_not_supp:1; unsigned is_selfpowered:1; unsigned deactivated:1; unsigned connected:1; };usb_gadget_driver:usb_gadget_driver - driver for usb 'slave' devices. usb 从设备驱动通用结构。作用:提供一个通用的usb gadget driver 模板,向下注册到udc,向上给functions driver提供bind 回调等。关注:bind 回调、function 驱动名、setup 处理请求struct usb_gadget_driver { char *function; /* String describing the gadget's function */ enum usb_device_speed max_speed; /* Highest speed the driver handles */ int (*bind)(struct usb_gadget *gadget, /* the driver's bind callback */ struct usb_gadget_driver *driver); void (*unbind)(struct usb_gadget *); int (*setup)(struct usb_gadget *, /* 处理ep0 request */ const struct usb_ctrlrequest *); void (*disconnect)(struct usb_gadget *); void (*suspend)(struct usb_gadget *); void (*resume)(struct usb_gadget *); void (*reset)(struct usb_gadget *); /* FIXME support safe rmmod */ struct device_driver driver; };usb_composite_driver:usb_composite_driver ,设备驱动的入口,用来管理设备配置信息,保存设备描述符。 重点:关注 bind 方法。struct usb_composite_driver { const char *name; /* 驱动名字 */ const struct usb_device_descriptor *dev ; /* 设备描述符 */ struct usb_gadget_strings **strings; enum usb_device_speed max_speed; unsigned needs_serial:1; int (*bind)(struct usb_composite_dev *cdev); /* bind 方法 */ int (*unbind)(struct usb_composite_dev *); void (*disconnect)(struct usb_composite_dev *); /* global suspend hooks */ void (*suspend)(struct usb_composite_dev *); void (*resume)(struct usb_composite_dev *); struct usb_gadget_driver gadget_driver; /* usb gadget driver */ };usb_composite_dev:内嵌gadget对象,以及usb 设备的一些配置和请求,主要用于初始化。struct usb_composite_dev { struct usb_gadget *gadget; struct usb_request *req; struct usb_request *os_desc_req; struct usb_configuration *config; /* usb 配置信息 */ /* OS String is a custom (yet popular) extension to the USB standard. */ u8 qw_sign[OS_STRING_QW_SIGN_LEN]; u8 b_vendor_code; struct usb_configuration *os_desc_config; unsigned int use_os_string:1; /* private: */ /* internals */ unsigned int suspended:1; struct usb_device_descriptor desc; /* 设备描述符 */ struct list_head configs; struct list_head gstrings; struct usb_composite_driver *driver; /* composite driver */ u8 next_string_id; char *def_manufacturer; /* the gadget driver won't enable the data pullup * while the deactivation count is nonzero. */ unsigned deactivations; /* the composite driver won't complete the control transfer's * data/status stages till delayed_status is zero. */ int delayed_status; /* protects deactivations and delayed_status counts*/ spinlock_t lock; unsigned setup_pending:1; unsigned os_desc_pending:1; };3.2 驱动剖析如图为一个通用的usb gadget 驱动剖析,框图中只列出了两个function,如果有多个function可以继续添加。关于udc控制器部分,,没有继续画下去,注意我们始终保持一个原则,【抓大放小】,把握重要的脉络即可。有关 usb 设备端驱动,比较复杂图片包含内容比较多,如果觉得不清楚,后台回复【gadget 驱动】获取高清图分层分块上下分层,左右分离的思想。设备功能驱动legacy 驱动入口functions 驱动实现Gadget 设备层:最重要的是compsite_bind 方法,承上启下的作用。udc 设备控制器层。usb 协议的真正处理。驱动走向向下:usb_composite_driver -> usb_gadget_driver->usb_udc向上回调:udc_bind_to_driver -> composite_bind -> webcam_bind 其中其主要作用的两个结构就是usb_gadget_driver 和 usb_compsite_dev。前者向下注册到udc list 里面,与udc控制器建立绑定关系;后者向上提供接口,供上层配置usb 设备的各种functions 和其他配置信息。代码分析注册usb_composite_drivermodule_usb_composite_driver(webcam_driver) module_driver(webcam_driver, usb_composite_probe, \ usb_composite_unregister)usb_composite_probeusb_composite_probe(webcam_driver); driver->gadget_driver = composite_driver_template; gadget_driver = &driver->gadget_driver; ... usb_gadget_probe_driver(composite_driver_template); udc_bind_to_driver(udc, driver); composite_driver_template->bind(udc->gadget, composite_driver_template); usb_gadget_udc_start(udc);composite_bindcomposite_bind(udc->gadget,composite_driver_template); cdev->gadget = gadget; composite_dev_prepare(webcam_driver,cdev); cdev->req = usb_ep_alloc_request(gadget->ep0, GFP_KERNEL); /* 申请端点0 */ cdev->req->complete = composite_setup_complete; cdev->driver = webcam_driver; usb_ep_autoconfig_reset(gadget); webcam_driver->bind(cdev);webcam_bindwebcam_bind(cdev); usb_get_function_instance("uvc"); try_get_usb_function_instance("uvc"); uvc_alloc_inst(); usb_add_config(); webcam_config_bind(); usb_get_function(); usb_add_function(); others_config_bind();其他关于function driver 我们这里没有详细介绍,这个框图只是一个通用的usb 设备驱动框架图,对于具体的usb function driver 我们这里没有做具体分析。以f_uvc简单举例,详细过程见内核源码。DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc);DECLARE_USB_FUNCTION_INIT(uvc, uvc_alloc_inst, uvc_alloc); usb_function_register(&uvcusb_func); list_for_each_entry(fd, &func_list, list) list_add_tail();DECLARE_USB_FUNCTION_INIT 一个通用的驱动模板,用来注册usb_function_driver,并添加到func_list上。#define DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc) \ static struct usb_function_driver _name ## usb_func = { \ .name = __stringify(_name), \ .mod = THIS_MODULE, \ .alloc_inst = _inst_alloc, \ .alloc_func = _func_alloc, \ }; \ MODULE_ALIAS("usbfunc:"__stringify(_name)); #define DECLARE_USB_FUNCTION_INIT(_name, _inst_alloc, _func_alloc) \ DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc) \ static int __init _name ## mod_init(void) \ { \ return usb_function_register(&_name ## usb_func); \ } \ static void __exit _name ## mod_exit(void) \ { \ usb_function_unregister(&_name ## usb_func); \ } \ module_init(_name ## mod_init); \ module_exit(_name ## mod_exit)4. 总结本文以拆解的方式,逐步剥离 usb 设备端驱动框架,带领大家来重新认识usb 设备端驱动,同时给出了一个 compsite 设备的通用驱动框架模型,并从源码层次分析整个驱动流程。有关USB 或者 其他类似的高级驱动,笔者有个建议,在初学时一点更要【把握主次,忽略细节】。比如一个复合的usb 设备可能包含,uvc,uac,hid,等等,视频有uvc function驱动和v4l2驱动,uac也有相应的驱动,衍生展开会非常复杂。所以当我们先掌握设备端驱动框架以及流程,等后面需要加入其他usb function 驱动再去研究其协议或者驱动,以及衍生驱动。
1. 什么是sqlite?sqlite 是一个小型,高速、高可靠性功能齐全的sql数据库引擎,并且是C语言开源的库,可以很方便的移植到各种嵌入式平台上。官网如下:地址:http://www3.sqlite.org/index.html最新版本:3.3.522. sqlite 交叉编译源码下载tar -zxvf sqlite-autoconf-3350200.tar.gz交叉编译cd sqlite-autoconf-3350200 mkdir pc_out ./configure --prefix=~/work/github/sqlite-autoconf-3350200/pc_out make make install安装完成文件如下:sqlite3:应用程序,直接可以打开和创建数据库include:头文件,开发用lib:sqlite3 的库,开发用share:共享文件和手册编译完成可以直接在pc上运行 sqlite3。mipsmips 交叉编译和 pc 差不多,不过有一点不同,我们需要配置工具链:准备工作:cd sqlite-autoconf-3350200 mkdir mips_out mkdir mips_out/glibc #创建glibc 目录 mkdir mips_out/uclibc#创建uclibc 目录 make distclean编译glibc: 第一步:配置configure。设置工具链和安装路径./configure --prefix=~/work/github/sqlite-autoconf-3350200/mips_out/glibc --host=mips-linux-gnu--prefix:指定安装路径。--host:指定工具链。注意,需要设置工具链的环境变量,否则检测不到工具链。配置完成后,可以打开Makefile 验证:第二步:编译和安装:make make install编译 uclibc: 第一步:配置configure。设置工具链和安装路径# 需要手动修改Makefile CFLAGS += -muclibc ./configure --prefix=~/work/github/sqlite-autoconf-3350200/mips_out/uclibc --host=mips-linux-gnu # 或者 CFLAGS += -muclibc ./configure --prefix=~/work/github/sqlite-autoconf-3350200/mips_out/uclibc --host=mips-linux-gnu--prefix:指定安装路径。--host:指定工具链。注意,需要设置工具链的环境变量,否则检测不到工具链。CFLAGS += -muclibc:指定uclibc 环境第二步:编译和安装make make installarmarm 平台交叉编译类似。需要注意的点如下:设置工具链的环境变量指定交叉编译工具链为arm平台所用的工具链。比如 arm-linux-gnu(只要前缀)3. sqlite 使用当交叉编译后,就可以直接把sqlite3 拷贝到板子上运行。注意,glibc环境拷贝glibc的,uclibc环境拷贝uclibc的。我们来看一个简单的示例:#include <stdio.h> #include <pthread.h> #include <dlfcn.h> #include <stdlib.h> #include <string.h> #include <sqlite3.h> #define DATABASE_NAME "MyDBDemo.db" #define DATABASE_TABLE_NAME "table1" #define DATABASE_TABLE_MEMBER "name text, age int, note text, count int" #define DATABASE_TABLE_LABEL "name, age, note, count" #define CREATE 0 #define INSERT 1 #define DELETE 2 #define SELECT 3 #define UPDATE 4 #define DROP 5 #define VACUUM 6 #define PRAGMA 7 struct sqlite_db_str { char *sql; }; struct sqlite_db_str db_sqlstr[] = { [CREATE] = { .sql = "CREATE TABLE " }, [INSERT] = { .sql = "INSERT INTO " }, [DELETE] = { .sql = "DELETE FROM " }, [SELECT] = { .sql = "SELECT " }, [UPDATE] = { .sql = "UPDATE " }, [DROP] = { .sql = "DROP TABLE " }, [VACUUM] = { .sql = "VACUUM " }, [PRAGMA] = { .sql = "PRAGMA " }, }; int main(int argc, char *argv[]) { int ret = -1; int k = 0; sqlite3 *db_ctx = NULL; char *zErrMsg = NULL; /*Step1: create database */ ret = sqlite3_open_v2(DATABASE_NAME, &db_ctx, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_SHAREDCACHE, NULL); if(ret){ printf("Open %s database errror!, ret: %d\n", DATABASE_NAME, ret); return -1; } /*Step2: create table */ char sql_buf[256] = {'\0'}; sprintf(sql_buf, "%s %s (%s);", db_sqlstr[CREATE].sql, DATABASE_TABLE_NAME, DATABASE_TABLE_LABEL); ret = sqlite3_exec(db_ctx, sql_buf, NULL, NULL, &zErrMsg); if( ret != SQLITE_OK ){ if( ret == 1 ){ /* table already exists */ printf("database [%s]: %s\n", DATABASE_NAME, zErrMsg); } else { fprintf(stderr, "SQL error: %s, ret: %d\n", zErrMsg, ret); sqlite3_free(zErrMsg); return -1; } } /*Step3: insert*/ for (k = 0; k < 10; k++) { char insert_buf[128] = {0}; sprintf(insert_buf, "'%s%d', %d, 'note_%d', %d", "Vinson", k, k, k, k); char sql_buf[1024] = {'\0'}; unsigned int sql_prefix_len = 0; sprintf(sql_buf, "%s %s (%s) VALUES (", db_sqlstr[INSERT].sql, DATABASE_TABLE_NAME, DATABASE_TABLE_LABEL); sql_prefix_len = strlen(sql_buf); memcpy(sql_buf+sql_prefix_len, insert_buf, strlen(insert_buf)); memcpy(sql_buf+sql_prefix_len + strlen(insert_buf), ");", strlen(");")); /* INSERT table1 (name age note count) VALUES (Vinson1,note1,1,1 )*/ ret = sqlite3_exec(db_ctx, sql_buf, NULL, NULL, &zErrMsg); if( ret != SQLITE_OK ){ fprintf(stderr, "SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); return -1; } if (ret < 0) { printf("insert table error! %d\n", ret); return -1; } } /*Step4: close*/ sqlite3_close(db_ctx); return 0; }编译:# 嵌入式设备 改成对应工具链:mips-linux-gun-gcc/arm-linux-gun-gcc gcc sqlite3_demo.c -o sqlite3_test -I./sqlite/include -L./sqlite/lib/ -lsqlite3 -ldl -lpthread -lm-I:指定sqlite 头文件包含-L:链接sqlite3 库。ldl:使用库需要lpthread:libsqlite3.a 使用。多线程相关lm:libsqlite3.a 使用。数学库执行:将生成的程序拷到板子上可直接运行。(./sqlite3_test)生成 MyDBDemo.db 验证:通过交叉编译生成的bin文件 sqlite3,可直接打开查看,我们sqlite3_test生成的数据库。./sqlite3 MyDBDemo.db查看数据库信息:.dump查看数据库表:.table退出:.quit用 Navicat Premium 打开对比查看。4. 总结本文,主要就是嵌入式端sqlite3移植进行介绍,关于数据库的语法和使用,留待读者自行研究。有关sqlite3的接口使用,本文只给出了简单的示例,详细应用,见官网使用说明。
1. 什么是Framebuffer?Framebuffer 字面意思就是帧缓存的意思,即显存,里面保存着一帧图像。事实上,对于嵌入式系统而言。没有真正意义上的显存,Framebuffer 是通过内存模拟出来的。LCD FrameBuffer 里的若干字节表示(具体根据驱动适配),LCD 屏幕上的一个像素点。RGB888:32bpp,占4字节,分别是A8、R8、G8、B8,一般只用其中低24位,高8位表示透明度。RGB565:16bpp,占2字节,分别是R5、G6、B5,比较常用的一种颜色RGB555:很少用。假设LCD屏幕分辨率是800x600,每个像素占4字节,那么framebuffer 大小就是:800 x 600 x 4 = 960000 字节framebuffer 显示原理如下:假设需要设置 LCD 中坐标(x,y)处像素的颜色,首要要找到这个像素对应的内存,然后根据它的 BPP 值设置颜色。假设 fb_base 是 APP 执行 mmap 后得到的 Framebuffer 地址,如下图所示:(x,y)像素起始地址=fb_base+(xres*bpp/8)y + xbpp/82. 为什么要有Frambuffer?思考一个问题,为什么要用Framebuffer?如图为LCD 驱动框架图:从软件层面分析:framebuffer 起着承上启下的作用,向上,为应用层提供通用系统调用(open(),ioctl(),mmap());向下,联接LCD控制器,之前对硬件进行操作。从硬件层面分析:用户只需要将数据写到framebuffer,硬件会自动刷新到屏幕上。3. 常用接口和数据结构3.1 常用接口打开设备:open()系统调用。通过 man 2 查看如下函数说明:ioctl 系统调用:函数原型:int ioctl(int fd, unsigned long request, ...);函数说明:fd :表示文件描述符;request:表示与驱动程序交互的命令,用不同的命令控制驱动程序输出我们需要的数据;… :表示可变参数arg,根据request命令,设备驱动程序返回输出的数据。返回值:打开成功返回文件描述符,失败将返回-1。3)mmap 系统调用:函数说明:3.2 相关数据结构fb_var_screeninfo:包含xres, yres, bits_per_pixel等信息,在后续会经常用到。4. 如何在LCD 上描点?4.1 LCD 显示原理当我们需要显示一个字母‘A’时,是通过判断点阵的每一个位数值状态,来填充颜色,达到显示字符效果。其中‘1’表示一种颜色,‘0’表示填充另一种颜色。如下图[1]8*16的点阵,只要有这个点阵,我们就可以在LCD上面描点,达到显示字符的效果。4.2 Framebuffer 操作说明framebuffer 操作如下流程:打开设备(open)获取屏幕参数信息(ioctl)分配显存(mmap)描点/写数据释放资源(unmmap)关闭设备(close)int fd_fb; struct fb_var_screeninfo var; /* Current var */ int screen_size; unsigned char *fbmem; unsigned int line_width; unsigned int pixel_width; int main(int argc, char *argv[]) { /*Step1: 打开设备 */ fd_fb = open("/dev/fb0", O_RDWR); if (fd_fb < 0) { printf("can't open /dev/fb0\n"); return -1; } /*Step2:获取设备参数信息 * xres:x 方向总像素 * yres:y 方向总像素 * bits_per_pixel:每个像素占多少位 */ if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var)) { printf("can't get var\n"); return -1; } /* Step3: 计算线宽,分配显存 */ /* line_width 每行占的字节 */ line_width = var.xres * var.bits_per_pixel / 8; pixel_width = var.bits_per_pixel / 8; screen_size = var.xres * var.yres * var.bits_per_pixel / 8; fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); if (fbmem == (unsigned char *)-1) { printf("can't mmap\n"); return -1; } /* Step4: 清屏: 全部设为黑色 */ memset(fbmem, 0, screen_size); /* Step5: 描点 */ lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/ /* Step6:释放资源*/ munmap(fbmem , screen_size); /* Step7:关闭设备 */ close(fd_fb); return 0; }4.3 描点实现描点的关键是计算点(x,y)位置对应的地址,然后直接指向fbmem即可向frambuffer 写入数据。/********************************************************************** * 函数名称:lcd_put_pixel * 功能描述: 在LCD指定位置上输出指定颜色(描点) * 输入参数:x坐标,y坐标,颜色 * 输出参数: 无 * 返 回 值: 会 ***********************************************************************/ void lcd_put_pixel(int x, int y, unsigned int color) { /* * 最主要的就是fbmem * 计算(x,y)位置的偏移,然后指向fbmem,这块直接映射到framebuffer 内存里 */ unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width; unsigned short *pen_16; unsigned int *pen_32; unsigned int red, green, blue; pen_16 = (unsigned short *)pen_8; pen_32 = (unsigned int *)pen_8; switch (var.bits_per_pixel) { /* 8bpp*/ case 8: { *pen_8 = color; break; } /*16 bpp */ case 16: { /* 565 */ red = (color >> 16) & 0xff; green = (color >> 8) & 0xff; blue = (color >> 0) & 0xff; color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3); *pen_16 = color; break; } case 32: { *pen_32 = color; break; } default: { printf("can't surport %dbpp\n", var.bits_per_pixel); break; } } }4.4 向 LCD 写入 英文写入英文的前提:描点函数已实现;具备该英文字符的点阵数据;/********************************************************************** * 函数名称:lcd_put_ascii * 功能描述: 在LCD指定位置上显示一个8*16的字符 * 输入参数:x坐标,y坐标,ascii码 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void lcd_put_ascii(int x, int y, unsigned char c) { /* fontdata_8x16 8x16 英文点阵数据 */ unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16]; int i, b; unsigned char byte; /* 8 x 16 的点阵, 16行 8列*/ for (i = 0; i < 16; i++) { byte = dots[i]; for (b = 7; b >= 0; b--) { if (byte & (1<<b)) { /* show */ lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */ } else { /* hide */ lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */ } } } }参考资料[1]如下图: 《嵌入式linux应用开发完全手册.韦东山》
LCD(Liquid Crystal Display):又称液晶显示器。广泛应用于嵌入式、移动端、pc 端。本文主要介绍常用 LCD 的简单分类。1.LCD 分类如下按信号类型分为 TTL/LVDS/EDP/MIPI 几大类别按材质分类分为(针对 TFT-LCD) TFT-TN/TFT-IPS/TFT-VA。接口类型分为:RGB 模式、SPI 模式、MDDI 模式、VSYNC 模式、DSI 模式、MCU 模式等2.LCD 常用的接口模式介绍RGB 模式RGB 模式就是我们通过说的 RGB 屏,以 RGB(TTL 信号)并行数据线传输,广泛的应用于 5 寸及以上的 TFT-LCD 中。串并行:串行;引脚:RGB 数据+时钟+控制引脚;数据为:RGB565、RGB666、RGB888。SPI 模式标准 spi 接口。也分为两类。SPI 控制信号 + RGB 数据线 和 SPI 控制/DATA。具体根据屏厂的手册。前者 spi 仅仅负责传输控制信号,后者 spi 传输控制和数据。spi 由于受传输速率限制,如果直接通过 spi 传输数据,可以看到,其实屏无法做大特别大。MDDI 模式高通公司 2004 年推出的接口。引脚包括 host_data/strobe, client_data/strobe 等。嵌入式中,一般用的不多。MIPI-DSI 模式MIPI-DSI 模式,即常说的 MIPI 屏。差分信号。适用于高速场合。MCU 模式MCU 模式 即我们常说的 MCU 屏,其标准名称是 I80(I8080),因广泛应用于单片机领域而得名。当然也有 M6800(摩托罗拉 6800)优点:控制简单,无需时钟同步。缺点:受限于内部 GRAM,很难做到大屏(3.8 以上)。显示速率慢,需要通过控制命令来刷新显示。8080I8080,又叫因特尔总线,是 MCU 模式中常用得一种总线,由数据总线和控制总线两部分组成。控制引脚如下:时序图:6800M6800,也叫摩托罗拉总线,其设计思想和与 I8080 一致。主要区别在于该模式下的读写控制位在一个 WR 引脚上,同时增加了一个锁存信号(E)。控制引脚如下:时序图:VSYNC 模式该模式在 MCU 模式基础上加上了一个 VSYNC 信号,应用于动画更新。在这种模式下,内部的显示操作和外部 VSYNC 同步,可以实现比内部操作更高速率的动画显示。但是该模式对速率有限制,那就是对内部 SRAM 写速率一点要大于读 SRAM 的速率。3.总结如下图从一个 datasheet 下摘录,基本包含了,嵌入式系统中,常见的接口屏。事实上对于我们开发者而言,更关心的往往是对外的接口。RGB:(DPI)RGB565/RGB666/RGB888MCU:I8080/M6800(8/9/16/18/24bit)SPI:3line/4lineMIPI-DSI:Data_N/P、Clock_P/N受限于接口引脚,速率,以及成本,我们会选择合适的屏,只需要关系何种接口、何种协议,然后有针对的去写其驱动即可。
1.安装 oh my zsh使用 curl 命令sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)使用 wget 命令sh -c "$(wget https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)" 详细安装介绍见旧文!2.安装 Powerlevel10k由于 oh my zsh 自带主题,响应速度很慢,故下载外部主题 powerlevel10k.1.github 安装git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k2. gitee 安装git clone --depth=1 https://gitee.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k3. 配置.zshrc在zsh的配置文件新增如下一项:ZSH_THEME=powerlevel10k/powerlevel10k4. 字体安装:安装 Nerd Font 字体有些系统可能需要安装字体,否则终端可能会出现乱码。nerd font 是支持 icon 最多的,可以直接在 nerd fonts github 下载安装 Hack Nerd Font。Powerlevel10k 作者推荐使用 Meslo Nerd Font 字体。相关安装见 github 或者官网连接。5. 配置 Powerlevel10k【自动配置】:终端输入 p10k configure。进入配置界面。 1)选择喜欢风格。 2)状态栏。 3)图标。 4)效果【个性化配置】:修改~/.pk10.sh。 1)左右栏图标显示。 左栏图标: 修改 POWERLEVEL9K_LEFT_PROMPT_ELEMENTS 右栏图标: 修改 POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS。若自动配置时没有配置可以不修改。 2)长路径折叠。 修改 POWERLEVEL9K_SHORTEN_DIR_LENGTH。 Powerlevel10k 默认将长路径折叠到只显示最上层和最底层,多少有些不方便,可以通过如下进行更改,推荐 2 或者 3。 3)颜色配置。 查看所有可用的颜色for i in {0..255}; do print -Pn "%K{$i} %k%F{$i}${(l:3::0:)i}%f " ${${(M)$((i%6)):#3}:+$'\n'}; done 根据需要修改目录显示的颜色3.安装 autojump 插件autojump是一个自动跳转目录的插件,可以可以记录之间 cd 过的目录路径,下次进入目录时不需要输入完整的目录路径。直接 j somedir 即可跳转,甚至目标目录的名称只输入开头即可。手动安装cd ~/.oh-my-zsh/plugins/ git clone https://github.com/wting/autojump.git cd autojump ./install.py or ./uninstall.py添加以下命令到 .zshrc,这样每次启动应用程序 zsh 时会自动运行[[ -s /home/wxyang/.autojump/etc/profile.d/autojump.sh ]] && source /home/wxyang/.autojump/etc/profile.d/autojump.sh4.总结基础工具:vimplus:搭配 vim8(编辑器)。配置见旧文oh_my_zsh(插件)。详细配置见旧文zsh-autosuggestions zsh-syntax-highlightingautojumpp10k(主题)基础效果如下:满足错误命令检测、目录/命令自动提示、模糊跳转目录,以及优化 oh_my_zsh 响应慢问题。
linux 系统调用,是以应用程序编程接口(API)的形式,内核提供有一些列服务供程序访问、包括创建新进程、执行 I/O、以及进程间通信创建管道等。一个最基本的 write 操作,是如何传递到内核呢?为什么说系统调用十分耗 CPU 资源?1.系统调用的本质系统调用的本质是一种异常,当调用一个系统调用时会触发 CPU 异常,CPU 进入异常处理流程。CPU 在异常处理流程中可以识别到本次异常是由于系统调用引起的,从而进入系统调用的异常处理流程中。2.异常异常是异常控制流的一种形式,任何打断当前正在执行程序的过程,都叫做异常。它一部分由硬件实现,一部分由操作系统实现。如图所示,为异常处理流程:在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表(exception table)的跳转表,进行一个间接的过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序(execption handler))。当异常处理完成后,根据引起异常的事件类型,会发生以下 3 种情况的一种:处理程序将控制返回给当前指令,即当时事件发生时正在执行的命令。处理程序将控制返回给下一条指令,如果没有发生异常将会执行下一条指令。处理程序终止被中断的程序。异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort):类别原因异步/同步返回行为中断来自 I/O 设备的信号异步总是返回到下一条指令陷阱有意的异常同步总是返回到下一条指令故障潜在可恢复的错误同步可能返回当前指令终止不可恢复的错误同步不会返回系统调用,就发生在陷阱这个异常中,又叫陷入内核。陷阱是有意的异常,是执行一条指令的结果。陷阱最主要的作用是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。3.系统调用过程分析用户程序经常需要向内核请求服务,比如读一个文件(read)、创建一个新的进程(fork)、加载一个新的程序(execve)、终止当前程序(exit)。为了允许对这些内核服务的访问,处理器提供了一条“syscall n”指令,当用户程序想要请求服务 n 时,可以执行这条命令。执行 syscall 指令会导致一个到异常处理的陷阱(trap),这个处理程序解析参数,并调用适当的内核程序。整个异常处理流程如下:下面以 Linux MIPS 为主,分析一下整个系统调用过程。3.1异常开始的地方CPU 中所有异常入口点都位于固定区域,不需要高速缓存的入口点位于 kseg1,需要高速缓存的点位于 ksg0。如图为 MIPS 架构异常入口点(参考《MIPS 体系结构透视》):3.2异常判断以 mips 平台为例。CPU0 的 Cause 寄存器中的 ExcCode(参考《MIPS 体系结构透视》),记录着各种异常 。ExcCode 是一个 5 位编码,告诉你发生了哪些异常:首先系统调用触发 CPU 异常时会陷入base+0x180(其他异常)地址进行处理。在进行具体异常处理前,要读取 Cause(ExcCode)寄存器判断何种异常。ExcCdoe 寄存器用来找出发生异常的类型,决定调用哪个异常处理流程。3.3异常向量表异常初始化是在内核中进行的。在 linux 内核中维护了一个异常向量处理函数表,这个表保存了所有异常处理入口点:异常向量表:unsigned long exception_handlers[32];异常初始化:/* init/main.c */ start_kernel(); /* arch/mips/kernel/traps.c */ -> trap_init();将各种异常的处理函数地址放入异常向量处理函数表中:将处理异常的代码放入 base+0x180 地址处异常处理代码(except_vec3_generic):except_vec3_generic 函数是异常处理函数,这个函数是广义上的异常,即不区分是系统调用还是中断。所有的异常处理都以这个函数为入口点。在这个函数中会去读取 CPU0 的 CAUSE 寄存器的 ExcCode 域,判断到底是什么触发了异常。然后根据 ExcCode 和之前配置的 except_handlers 异常向量处理函数表找到对应异常处函数进行处理。3.4syscall 调用系统调用号每个系统调用被赋予一个系统调用号。这样,通过独一无二的系统调用号,就可用关联系统调用。当用户空间的进程执行一个系统调用,这个系统调用号就用来指明到底执行哪个系统调用,进程不会提及系统调用的名称。系统调用号相当重要,一旦分配,就不能再有任何变更,否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。linux 系统有一个未实现的系统调用 illegal_syscall(),它除了返回-ENOSYS 外,不做其他工作,这个错误号是专用针对无效的系统调用而设置的。linux 内核系统调用号:定义在 arch/mips/include/uapi/asm/unistd.hglibc 系统调用号:/opt/mips-gcc540-glibc222-64bit/mips-linux-gnu/libc/usr/include/asm/unistd.hglibc 系统调用号和 linux 内核系统调用号是一一对应关系。系统调用号从 4000 开始。系统调用号最多可以支持到 4999,即 1000 个系统调用。系统调用表内核记录了系统调用表中的所有已注册的系统调用,存储在 sys_table_call 中,每一种体系结构中(不同的平台),都明确定义了这个表。这个表为每一个有效的系统调用指定了唯一的系统编号。如图为mips32位平台下(arch/mips/kernel/scall32-o32.S)sys_table_call。系统调用表是一张指向实现各种系统调用的内核函数的函数指针表,该表可以基于系统调用号进行索引,来定位函数地址,完成系统调用。系统调用之 glibcglibc 在应用程序和内核之间起了一个桥梁的作用。在应用程序中,我们操作 open、write、read、ioctl 等函数,其实是对系统调用的封装。glibc 将诸多系统调用进行封装,是我们可以以函数的形式,方便的调用系统调用:glibc 如何传递到内核应用程序如果想要调用内核的一个系统调用,只能通过上层应用和内核都认可的系统调用号来完成。所以在 glibc 中,如果想要调用内核的一个系统调用,唯一的方法就是:将想要的系统调用号和需要传入的参数通过特定的方法传入到内核中。使用 syscall 指令传递syscall 是 MIPS(其他平台也有这条指令)的一条指令,该指令的作用就是产生一个“系统异常”,该指令执行完成后,系统会进入异常处理流程,并且 ExcCode 被置为 8,指明为系统调用:在 MIPS Linux 中系统调用约定如下:v0:保存系统调用号。a0~a3:保存系统调用的前四个参数,多余四个使用栈传递。glibc 处理系统调用流程如图是 glibc 中对 MIPS Linux 中带一个参数的系统调用处理流程(internal_syscall1)参数展开如下:input:NR_name,系统调用名arg1:携带的一个参数。linux 内核系统异常处理CPU 进行异常处理后,根据 ExcCode 的值判断是系统调用产生的异常,此时就会进入 handle_sys 进行处理:判断系统调用号是否符合规范。从系统调用表中取出系统调用的地址和支持的参数个数。判断系统调用是否需要参数。执行系统调用。3.5系统调用完整路径4.总结本文以 Linux MIPS 平台为主,详细分析了 linux 的系统调用过程。自上而下,从应用(app)到运行时(Runtime)再到内核、最后深入 CPU 寄存器分析整个系统调用处理流程。现在回过头,再看前文的两个问题,应该就明白了。系统调用的本质是异常;系统调用俗称“嵌入内核”,那么应该了解了,为什么都不推荐轮询操作 IO 了吧(一次调用过程,流程非常复杂,频繁的调用 IO 操作,会使得 CPU 不停的进入异常处理流程,打断当前的操作,会大大的消耗 CPU 的资源)
1.GNU / GCC简介1.1GNUGNU工具链(GNU Toolchain)是一组用于开发应用程序和操作系统的变成工具的集合,这些工具构成了一个完整的系统。GNU工具链包括GCC,GNU Binutils,GNU m4,GNU make等部分。此处主要介绍GCC。1.2GCCGCC原来代表“ GNU C Compiler”的意思。但是通过GCC的发展,现在不仅仅支持C语言,也支持C ++,Java,Objective-C等。因此,GCC被重新定义为“ GNU编译器集合”,即GNU编译器套件。2.什么是GCC?(GNU Complier集合)GCC(GNU Complier Collection)是一套完整的工具链。包括gcc,g ++,ar,as,ld,objcopy,objdump等。不同的平台都有一套完整的工具。2.1工具链组成pc端:x86_64-linux-gnu-。否则就是gcc嵌入式(MIPS):mips-linux-gnu-。arm对应的是arm-linux-gnu,不同的平台取代不同。注意:这里嵌入式平台分为glibc 和 uclibc 之分。2.2主要工具介绍名称说明gccC 编译器,事实上最终调的是cc1 和 汇编器和链接器。g++C++ 编译器,事实上最终调的是cc1plus 和汇编器和链接器。常用,事实上gcc 也可以编c++程序。cppC和C++预处理器,同 gcc -E 选项c++用法同g++addr2line是一个可以将指令的地址和可执行映像转为文件名、函数名和源代码行数的工具。在程序崩溃时,可以快速定位代码位置。调试ar用于建立、修改和提取档案文件。档案文件经常被用作程序库文件as汇编器ld链接器gdbGNU 调试器nm用于列出二进制文件 (包括库文件和可执行文件) 中的符号,这些符号可以是函数、全局变量、静态变量等objcopy将目标文件从一种二进制格式复制和翻译到另外一种二进制格式objdump用于列出关于二进制文件的各种信息readelf读取ELF 格式的显示信息strip用于移除目标文件中的符号,以及其他调试所需要的信息size用于列出目标文件或者档案文件各段的大小3.总结本文简单介绍了,gcc工具链以及常用的工具,梳理一下各工具的用途。
接上文,本文主要介绍 linux 目标文件的组成,逆向分析 ELF 文件结构1.目标文件的构成编译器编译源码后生成的文件叫目标文件(Object File)。目标文件从结构上讲,它是已编译后的可执行文件格式,只是还没有经过链接过程,其中有些符号或有些地址还没有调整。目标文件本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。1.1格式目标文件就是源代码编译之后但未进行链接的哪些中间文件(Windows 下 是 .obj,linux 下是.o)。它与可执行文件的内容和结构基本相同。目标文件和可执行文件一般采用同一种格式,这种存储格式为 ELF(linux Executable Linkable Format),window 下 是 PE(Portable Executable)在 linux 下 除了目标文件和可执行文件采用 ELF 格式外,静态库文件、动态库文件也都按照可执行文件 ELF 的格式存储。ELF 文件类型说明实例可重定位文件(Relocatable File)此类文件包含代码和数据、可被链接为可执行文件或共享目标文件,静态链接库被归为此类Linux 下的 .o:hello.o可执行文件(Executable File)此类文件包含了可以直接执行的文件/bin/bash等共享目标文件(Share Object File)此类文件包含了可以直接执行的文件linux 的.so、/lib/x86_64-linux-gnu/libc-2.23.so核心转储文件(Core Dump File)当进程意外终止,系统生成的核心转储文件Linux 下的 core dump文件linux 可以通过 file 命令查看。目标文件和可执行文件跟操作系统和编译器密切相关,不同平台下的格式会有些差异。目标文件:文件格式:ELF 64bitLSB:小端x86-64:平台 (和工具链有关)relocatable:可重定位not stripped:没有 stripped可执行程序:文件格式:ELF 64bitLSB:小端executable:可执行x86-64:平台statically linked:静态链接not stripped:没有 stripped动态库:文件格式:ELF 64bitLSB:小端share object:共享文件x86-64:平台dynamically linked:动态链接1.2组成目前文件中的内容至少有编译后的机器指令代码和数据,除了这些,目标文件中还包含了链接时所必须的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按照不同的属性,以段(segment)的形式存储。代码段(.text):源代码编译过后的机器指令。数据段(.data):全局变量和局部静态变量被放在数据段。只读数据段(.rodata):const 修饰的变量和其他字符串常量。bss 段:为未初始化的符号,预留足够的空间。未初始化的变量在 bss 段。其他段。2.目标文件的分析下面我们根据一段代码来分析目标文件的内部结构(以 MIPS 平台为例):2.1ELF 文件头ELF 文件开头是一个头文件,它描述的是整个文件的属性,包括:文件的类型、目标硬件、目标操作系统等信息。readelf -h 可读取 elf 文件头。头文件包含如下:ELF 魔数。文件机器字节长度。数据存储方式。版本。运行平台。ABI 版本。ELF 重定位类型。硬件平台。硬件平台版本。入口地址(目标文件入口地址为 0,只有相对位置)。程序的入口和长度。段表的位置和长度。目标文件 ELF 头可执行程序 ELF 头2.2段表头文件之后,紧接着是一个段表,段表是一个描述文件各个段的数组。描述了文件中各个段在文件中的偏移位置以及段的属性。(readelf -S, objdump -h)主要的段如下:代码段(.text)。数据段(.data)。bss 段。只读数据段(.rodata)。.comment(符号表段).symtab符号表(.shstrtab):Section String table(重定位代码段).rel.text等各段详细信息及字节对齐:最终组成结构如下。中间Section Table 和 其他某些段因为对齐原因,会有若干字节的间隔。整个目标文件的大小刚好是 1588 字节。代码段C 语言编译后的机器指令,都保存在代码段(.text)。我们可以通过 objdump 来查看段的信息。-s 可以将所有段以 16 进制打印出来。-d 可以将所有包含指令的段反汇编。可以看到代码段保存的是 f 和 main 的指令。数据段数据段(.data)保存的是哪些已经初始化(非零)的全局变量(静态变量和非静态变量)和局部静态变量。gint_val 和 static_val 存在数据段。0x64 和 0x65 刚好对应 100 和 101(ASCII d 和 e)数据段是 16 字节对齐所以刚好占 16 个字节大小。对齐和平台有关。只读数据段只读数据段(.rodata),保存的是只读数据。一般是程序中 const 修饰的只读变量 和字符串常量(包括 printf 函数中的格式化字符串%d)。可以看到,我们可以通过优化.rodata 段的大小,进而优化程序的大小。bss 段bss 段(.bss),用来记录所有未初始化的全局变量(或者零初始化)和局部静态变量大小总和,然后为其预留位置。未初始化的全局变量(或者零初始化)和局部静态变量,因为都是 0,所以在.data 段开辟存储空间存储 0 是没有必要的。符号表段符号表(.symtab),以数组结构的形式保存符号信息(函数和变量),对于函数和变量符号值就是他们的地址。(readelf -s)f 和 main 函数的 Ndx 对应的值是 1,表示在.text 段(.text 段在段表中的索引是 1),类似是 FUNC,value 分别是 0x00000000 和 0x00000040,表明两个函数指令字节码的首字节分别在.text 的 0x00000000 和 0x00000040 偏移处。printf 的 Ndx 是 UND,表明这个符号在 object_file.o 里面没有被定义,仅仅是引用。static_val.1843 和 gint_val 两个符号的 Ndx 都是 3,说明他们都被定义在数据段。value 分别是 0x00000000 和 0x00000004,表明两个函数指令字节码的首字节分别在.data 的 0x00000000 和 0x00000004 偏移处。重定位段表重定位表也是一个段,用于描述在重定位时连接器如何修改相应段里的内容。对于.text,对应的重定位表是.rel.text。使用objdump -r 查看重定位表。自定义段表正常情况下,GCC 编译出来的目标文件会被放到.text 段,全局变量和静态变量会被放到.data 和.bss 段。但是有些时候我们希望有些变量或者代码可以放到我们指定的段中去,以实现某些特定的功能。GCC 提供了一种扩展机制,可以指定段。变量:__attribute__((section("start_var_init"))) int init_status = 1;函数: __attribute__((section("start_fun_init"))) void start_func() { //init code return ; }在全局变量或者函数前加入 _attribute_((section("name"))) 属性就可以把相应的变量或者函数放入到”name“段里。示例: 3.总结本文主要介绍了,ELF 文件组成结构,通过分析目标文件的组成,来理解程序的具体分布。了解目标文件的各段内容和作用,有助于我们提高对程序的掌控力。比如优化程序对应用程序进行加解密、调试等等。3.1相关命令命令说明readelf -h读取ELF文件头readelf -S查看段的属性readelf -s查看符号表objdump -h查看段的属性objdump -s将所有段的内容以十六进制打印出来objdump -d将所有包含的指令反汇编objdump -r查看重定位段3.2分段说明段名说明.text代码段。存放可执行文件的指令,这部分区域在程序运行前就已经确定。通过 objdump -s -d 查看.data数据段。保存已经初始化(非零初始化)的全局变量和静态局部变量.bssbss段。未初始化(零初始化)的全局变量和静态局部变量保存在bss段,准确来说.bss段为他们预留了位置,等到最终链接时在分配到.bss段(具体和编译器有关).rodata只读数据段。存放的是只读数据,一般是程序里面的只读变量(const修饰的变量)和字符串变量(printf 的格式化字符也算).comment存放的是编译器的版本信息.symtab符号表段。用来定位、重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。.shstrtab字符串表段。存放着所有符号的名称字符串.dynamic动态链接信息。...其他
什么是 8080 协议?8080 时序也叫因特尔总线,一般应用在mcu(mpu)模式的lcd上。Inter总线控制线有四根:RD:写使能WR:读使能DC(RS):数据/命令CS:片选LCD控制及传输数据所需要的管脚:管脚名称功能描述CS片选信号DC(RS)数据或者命令管脚(1:数据读写,0:命令读写)WRMCU(MPU)向LCD写入数据控制线,上升沿有效,写数据时 RD拉高RDMCU (MPU) 从LCD读数据控制线,上升沿有效,读数据时,WR拉高DB[x:0]8/9/16/18bit 双向数据总线,一般8位MCU接口用的比较多RST硬件复位 LCD 信号BLLCD背光信号IM2/IM1/IM0接口控制. IM2=0 串行,IM2 =1,IM1=0,IM0=0,6800/8080 8bit 并行接口;IM1=0,IM0=1,6800/8080 16bit 并行接口;IM1=1,IM0=0,6800/8080 9bit 并行接口;IM1=1,IM0=1,6800/8080 18bit 并行接口;P686800/8080 选择。0 表示8080,1 表示6800读写数据读时序图。通用时序图如下: 读数据的过程:CS 拉低,选中DC/RS 为高(读数据)WR 为高,禁止写在RD的上升沿,读线上的数据(D[0:7]),假设8位 8080并口CS 拉高,取消片选伪代码:LCD_CS = 0; //开始片选 LCD_DC = 1; //读数据 LCD_WR = 1; //禁止写 LCD_RD = 0; //开始读 data = DATAIN();//读取数据 LCD_RD = 1; //结束读 LCD_CS = 1; //结束片选写入数据写时序图。通用时序图如下:写数据的过程:CS为低,选中RD为高, 禁止写DC/RS为高(写数据,写命令拉低)在WR的上升沿,使数据写入到 驱动 IC 里面CS为高,结束一组数据读取伪代码:LCD_CS = 0; //开始片选 LCD_RD = 1; //禁止读 LCD_DC = 1; //写数据 DATAOUT(Data); //输出数据 ,先准备好数据,然后上升沿一次性更新到lcd LCD_WR = 0; //写入开始 LCD_WR = 1; //写入结束 LCD_CS = 0; //结束片选实列演示:GPIO 模拟读写时序以GC9106 这块LCD 驱动IC为例,介绍一下,如何根据datasheet,去模拟读写操作,方便后续遇到新的屏幕,可以快速入手。事实上,大部分时候,我们的主控一般都会存在lcd控制器,支持读写操作,只需要配置寄存器即可。不过通过GPIO模拟,对整个过程会有更新的认识。第一步:根据硬件确认屏幕所接接口,确认引脚及协议:以8080 8位并行为例协议:8080时序 引脚:LCD_CS、LCD_RD、LCD_WR、LCD_DC、LCD_Data[7:0]第二步:根据 datasheet 阅读读写时序读时序 驱动IC会在下降沿产生数据,主控(MCU)在上升沿从D[7:0] 读数据整个读数据的过程,分为两步:写入一个command address, 读数据。写入cmd: 读数据:根据时序图,真正读数据的过程中,第一个周期的数据是无效的,这个可以很好的解释command列表中,为何第一次数据都是dummy data写时序 主控(MCU)会在下降沿产生数据,驱动IC在上升沿从D[7:0] 读数据 整个写数据的阶段分为两个:写命令和写数据,唯一的区别是DC不一样。 第三步:查看command列表。这里以读ID为列(04H) 阅读command信息根据timing,先写入一个cmd(04H)紧接着,读四次数据。舍弃第一个数据(dummy data)注意:初始状态和初始值。如果读不出来数据,可能是上电时序的问题,这个时候可以通过硬件GPIO 复位一下lcd,然后在读取数据。write_cmd(0x04); read_data(); //dummy data ID0 = read_data(); ID1 = read_data(); ID2 = read_data();第四步:模拟基本读写时序操作。根据通用8080协议时序即可 此处用伪代码表示思路即可:static void write_lcd_dc(int isCmd, unsignedint value) { LCD_CS = 0; /* 拉低 */ LCD_RD = 1; /* 禁止读 */ if(isCmd == 1) { LCD_DC = 0; /* 拉低 写命令 */ } else{ LCD_DC = 1; /* 拉高 写数据 */ } data_out(value); /* 准备数据 */ LCD_WR = 0; /* 拉低 */ delay10us(); /* 上升沿更新数据*/ LCD_WR = 1; /* 拉高*/ delay10us(); LCD_CS = 1; /*拉低, 取消片选*/ } static void data_out(unsignedint value) { /* 假设 data[0:8] 在一组GPIO上,并且连续 * 设置data[0:8] 为输出状态,切默认都输出0 */ int i = 0; for(int i = 0; i < 8; i++) { LCD_Data[i] = ((value >> i) & 0x01); } } static unsigned int read_lcd_data() { int ret = 0; LCD_CS = 0; /* 拉低,选中 */ LCD_WR = 1; /* 拉高, 禁止写 */ LCD_DC = 1; /* 拉高,表明将要读数据 */ LCD_RD = 0; /* 拉低, 开始读数据*/ delay10us(); ret = data_In(); /* 读数据*/ delay10us(); LCD_RD = 1;/* 拉高, 结束读数据 */ delay10us(); LCD_CS = 1; /* 取消片选*/ return ret; } static unsigned int data_in() { int ret = 0; /* 假设 data[0:8] 在一组GPIO上,并且连续 * 设置data[0:8] 为输入状态 */ int i = 0; for(int i = 0; i < 8; i++) { ret |= (gpio_get_value(LCD_Data[i]) << i); } return ret ; }总结本文介绍了8080协议的基础时序,以及通用的读写操作,旨在了解学习8080协议。然后通过实际的lcd 驱动IC 应用举例,介绍如何阅读 lcd datasheet.通过GPIO 模拟读写操作,达到对整个协议的理解。事实上,大多数情况下,我们不需要亲自模拟时序,调试一款新屏时,一般通过配置lcd控制器,会自动模拟时序,只需要将原厂的setting,按照对应的规则,加入到驱动中即可。当然,也可以通过gpio模拟读写,将lcd的setting,通过自己模拟的读写接口,设进去。
驱动框架总览lcd 驱动框架总览图如图所示,本质上是一个字符设备驱动。应用程序通过open函数打开设备驱动,通过read/write进行读写。app : open(/dev/f0) kernel: fb_open fb_info = get_fb_info(fbidx); fb_info->fb_ops->fb_open最关键的地方在于fb_info:fb_ops 里面的接口函数最终实际上是调用fb_info->fbops填充的函数。fb_mem是linux 内核抽象的一层通用的fb 框架。对于不同的芯片平台,lcd 驱动真正实现是在xxxfb.c。在不同的平台加载自己的lcd驱动时,resgisterframebuffer 填充fbinfo。驱动分析fbmem 层:抽象出来通用的字符设备框架。用户直接操作的驱动。创建一个字符设备驱动填充 fb_ops平台驱动fb_probe framebuffer_alloc fb_info 填充 register_framebuffer //其他硬件相关设置fb_info:关键结构体介绍struct fb_info { atomic_t count; int node; int flags; struct mutex lock; /* Lock for open/release/ioctl funcs */ struct mutex mm_lock; /* Lock for fb_mmap and smem_* fields */ struct fb_var_screeninfo var; /* 可变参数*/ struct fb_fix_screeninfo fix; /* 固定参数 */ struct fb_monspecs monspecs; /* Current Monitor specs */ struct work_struct queue; /* Framebuffer event queue */ struct fb_pixmap pixmap; /* Image hardware mapper */ struct fb_pixmap sprite; /* Cursor hardware mapper */ struct fb_cmap cmap; /* Current cmap */ struct list_head modelist; /* mode list */ struct fb_videomode *mode; /* current mode */ #ifdef CONFIG_FB_BACKLIGHT /* assigned backlight device */ /* set before framebuffer registration, ¦ remove after unregister */ struct backlight_device *bl_dev; /* Backlight level curve */ struct mutex bl_curve_mutex; u8 bl_curve[FB_BACKLIGHT_LEVELS]; #endif #ifdef CONFIG_FB_DEFERRED_IO struct delayed_work deferred_work; struct fb_deferred_io *fbdefio; #endif struct fb_ops *fbops; struct device *device; /* This is the parent */ struct device *dev; /* This is this fb device */ int class_flag; /* private sysfs flags */ #ifdef CONFIG_FB_TILEBLITTING struct fb_tile_ops *tileops; /* Tile Blitting */ #endif char __iomem *screen_base; /* Virtual address */ unsignedlong screen_size; /* Amount of ioremapped VRAM or 0 */ void*pseudo_palette; /* Fake palette of 16 colors */ #define FBINFO_STATE_RUNNING 0 #define FBINFO_STATE_SUSPENDED 1 u32 state; /* Hardware state i.e suspend */ void*fbcon_par; /* fbcon use-only private area */ /* From here on everything is device dependent */ void*par; /* we need the PCI or similar aperture base/size not ¦ smem_start/size as smem_start may just be an object ¦ allocated inside the aperture so may not actually overlap */ struct apertures_struct { unsignedint count; struct aperture { resource_size_t base; resource_size_t size; } ranges[0]; } *apertures; bool skip_vt_switch; /* no VT switch on suspend/resume required */ };主要参数:fb_varscreeninfo:可变参数。记录屏幕分辨率,屏幕色域等fb_fixscreeninfo:固定参数。记录屏幕缓冲区的物理地址和物理长度等。fbops:记录了对底层硬件操作的函数指针,充当file_operations 角色。整个调用过程大体如下:应用层:open(/dev/fb0) ioctl 驱动:-> fb_open -> fb_ioctl -> fb_info->fbops->open -> fb_info->fbops->ioctl ... ...总结本文主要介绍了linux lcd 驱动整体框架,梳理 linux lcd 通用框架和设备平台之间的关系,关于LCD 具体硬件部分的驱动介绍暂未介绍,不同屏幕厂家也略有差异。希望本文对你有所帮助!
valgrind 介绍valgrind 是一个 GPL 软件,用于 Linux ( For x86 ,amd64 and mips ...) 程序的内存调试和代码分析。使用 valgrind 的工具包,可以自动检测许多内存管理和线程的bug,让你的程序运行的更加稳定。valgrind 的工具包包含多个工具:memcheck:内存检查使用未初始化的内存:Use of uninitialised memory使用已释放的内存 :Reading/writing memory after it has been free’d使用超过malloc分配的内存空间:Reading/writing off the end of malloc’d blocks对堆栈的非法访问:Reading/writing inappropriate areas on the stack申请的空间是否有释放:Memory leaks – where pointers to malloc’d blocks are lost forevermalloc/free/new/delete 申请和释放内存的匹配:Mismatched use of malloc/new/new [] vs free/delete/delete []src和dst的重叠:Overlapping src and dst pointers in memcpy() and related functionscallgrind:收集程序运行的一些数据,函数调用关系等。还可以有选择的进行cache模拟。在运行结束,他会把分析数据写入一个文件,callgrind_annotate 可以把这个文件内容转换成可读形式。cachegrind:它模拟 CPU中的一级缓存I1,D1和L2二级缓存,能够精确地指出程序中 cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。helgrind:它主要用来检查多线程程序中出现的竞争问题。Helgrind寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。massif:堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。valgrind 安装和编译下载wget http://valgrind.org/downloads/valgrind-3.12.0.tar.bz2编译及安装以 mips,uclibc环境为例--host:指定交叉编译工具链,默认glibc--prefix:指定安装路径 -muclibc:指定uclibc环境 -D__UCLIBC__:define __UCLIBC__。valgrind 需要定义这个宏否则运行报错。./autogen.sh ./configure --host=mips-linux-gnu CFLAGS="-D__UCLIBC__ -muclibc" --prefix=/home_a/wxyang/nfsroot/t30/valgrind/ make make install安装工具集valgrind 应用实例分析启动开发板,设置环境变量:export VALGRIND_LIB=/mnt/t30/valgrind/lib/valgrind export PATH=/mnt/t30/valgrind/bin/:$PATH启动应用程序:valgrind --log-file=valgrind.log --tool=memcheck --leak-check=full --show-reachable=yes ./test--tool=memcheck:设置启动工具为memcheck --leak-check=full:完全检查内存泄漏 --log-file=valgrind.log:输出日志文件 --show-reachable=yes:检测控制范围之外的泄漏注意:应用程序编译时加上-g, 尽量不要用O2优化(用-O0),同时不要用 strip 压缩,否则看不到详细信息。日志分析:常见错误malloc/free: in use at exit :内存在退出前没有释放invalid write of size :非法写入内存,一般为数组越界invalid read of size :非法读内存:一般为数组越界definitely lost /possibly lost /still reachable in loss record:内存未释放definitely :确认丢失。程序中存在内存泄露,应尽快修复。indirectly:间接丢失。当使用了含有指针成员的类或结构时可能会报这个错误 。possibly:可能丢失。大多数情况下应视为与"definitely lost"一样需要尽快修复。still reachable:可以访问,未丢失但也未释放。如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源。suppressed:已被解决。出现了内存泄露但系统自动处理了。可以无视这类错误。invalid free()/delete/delete[] :同一指针被多次释放source and destination overlay :一般是使用strncpy,memcpy引起syscall param contains uninitialized byte:调用系统函数时传入了未初始化的变量conditional jump or move depends on uninitialized value :条件判断时使用了未初始化的变量access not with mapped region/stack overflow :栈溢出mismatch free()/delete/delete[]/new :delete/malloc/free搭配错误错误举例indirectly lost:如图可以追溯到具体文件及函数调用,明显是cjson使用有问题。still reachable:总结本文简单介绍 valgrind 安装和使用,以及对内存泄漏简单分析,算是提供一种排查思路;通常自己写的程序一般有些许掌控力,不过对于大型项目,多人协助,以及引入的第三方库,难免会遗留一些bug。这个时候就需要一些工具辅助排查了,能够大大提升效率。希望本文对你,有些许帮助。
测试环境 CPU:Inter(R) Core(TIM) i3-2120 CPU@3.3GHz内存:8GB操作系统:Deepin显示调用事务利用事务的互斥性,在批量操作时显示开启事务,操作结束后提交事务,那么所有操作将只执行一次事务,大大提高IO效率。sqlite3_exec(db, "BEGIN;", 0, 0, NULL); for( i = 0; i < 10000; i++) { ... sqlite3_exec(db, sqlcmd, NULL, NULL, &zErr); } sqlite3_exec(db, "COMMIT;", 0, 0, NULL);磁盘同步 (synchronous)synchronous 获取或者设置当前磁盘同步模式,该模式用于控制SQlite写入磁盘的时机。Pragma 值说明0或者OFF不进行同步。写入数据后传递给操作系统则完成操作,类似mmap,剩下的交给操作系统完成1 或者NORMALsqlite2 的默认模式,在关键磁盘的每个序列后同步。不像FULL模式那么频繁刷盘,有小几率在电源故障或者磁盘不可用时导致数据库损坏2 或者 FULLsqlite3 默认模式,在每个关键磁盘操作后同步,性能差。数据库在紧急时刻暂停以确定数据写入磁盘,使得系统崩溃或者电源出问题时,确保数据库重启不会损坏当 我们对数据丢失情况不太敏感,可将 synchronous 设置为 off,写入性能可提高3倍。内存模式(temp_store)Pragma 值说明0 或者DEFAULT使用编译时的C预处理宏 TEMP_STORE来定义储存临时表和临时索引的位置1 或者FILE则存放于文件中。temp_store_directory pragma 可用于指定存放该文件的目录2 或者 MEMORY临时表和索引则存放于内存中journal_model 设置journal_mode 获取或者设置控制日志文件如何存储和处理的日志模式。journal 为的是数据库事务的rollback操作,数据库begin trans写入时,首先写入journal 文件中,commit 操作时,根据journal-model来处理日志文件。如果在commit之前由于断电等原因造成无法commit,当再次启动时,可通过journal文档做回滚操作,保证数据库的完整性和一致性。Pragma 值说明DELETE默认模式。事务结束时,日志文件删除TRUNCATE日志文件被截断为0字节长度PRESIST日志文件保留在原地,但头部被重写,表明日志不再有效MEMORY日志文件记录在内存中,而不是磁盘OFF不保留任务日志WALwrite ahead log。DELETE:读写操作时DEL模式要处理各种锁。写操作是独享的,写阻塞读;读完成时才能写,读阻塞写。WAL:修改不直接写入数据库文件中,而是直接一个WAL文件中,若事务失败,WAL记录被忽略;若事务成功,随后在某个checkpoint时间点写回数据。若要继续提升性能,可修改checkpoint.page_size 和 cache_sizepage_size:分页大小。默认page_size = 4096。其值为512、1024、2048、4096、8192、16384、32768、65536.cache_size:表示在缓存中的页面数,内置页面缓存的默认大小为 2,000 页,最小尺寸为 10 页。注意:通 PRAGMA page_size/cache_size 能够查询当前页大小和缓存size;PRAGMA cache_size = xxx ,可动态设置缓存大小,仅当前数据库链接有效;page_size:在创建数据库表时设置即可生效。sqilte3.5.8 以后,通过PRAGMA cache_size =xxx 后,执行 VACUUM, 也可动态修改页大小。如果未执行VACCUM,页大小修改无效。page_size 和 cache_size 不是越大越好。具体要结合硬件缓存以及数据库记录。page_size 决定着数据库最小储存单元page的大小。当查询记录大小大于page_size 时,此时需要多次寻址才能完成本次操作,如果page_size 大于 查询记录大小,一次寻址即可。理论上page_size 和 cache_size 越大越好,实际根据应用场景和硬件资源来调整这两个参数。当缓存足够大的时候,一次查询能够直接命中数据库最后一条记录,在向上调整就没有效果了;当缓存和内存资源紧张,特别是嵌入设备,根据实际场景调整,page_size 设置尽量接近需要频繁查询的记录大小。然后调整cache_size,这样也可以一定程度控制数据库大小。以下对轻量级数据库做了一个简要测试:mmap_sizemmap对I/O性能的提升无需赘言,尤其是对于读操作。SQLite也在OS层封装了mmap的接口,可以无缝地切换mmap和普通的I/O接口。只需配置PRAGMA mmap_size=XXX即可开启mmap。其他优化表的结构:比如将 blob 字段,放在数据库末尾,尽量降低blob大小;提升 CPU 和 DDR 频率,对于嵌入式平台,CPU频率对数据库查询速率影响较大。适当的提高CPU频率会大大提升查询性能;优化应用程序负载,降低CPU 和 DDR 负担,在测试时发现,自己写的测试demo速度优化很明显,一加入到项目中,发现查询速度又降下来了,排查发现有个线程对CPU的资源消耗非常大,适当的降低其占用CPU时间,查询速度立马起来了。总结本文介绍了sqlite 常见的几种优化性能的方式:事务磁盘同步temp_storejournal_modepage_size 和 cache_sizemmap_size结合自身需求,选择适合自己的方式!最后放一个sqlite c源码链接,感兴趣的自行下载:http://www3.sqlite.org/index.html
最近项目中遇到 Base64 编码问题,花了点时间总结了一下。什么是Base64编码?base64 是网络上最常见的用于传输8bit字节代码的编码方式之一,是一种基于64个可见字符来表示二进制数据的方法。通过3个8bit字节( 3 x 8 = 24 )编码成4个6位字节(4 x 6 = 24),在在每个6位字节前补两个0,形成4个8字节形式。为什么要有Base64编码?有些网络上传输渠道并不支持所有字节,比如邮件,ASCII 控制字符 、中文、图片二进制数据等。最好的方法是在不改变传统协议的情况下,开辟一种新方案来支持二进制文件的传输。把不可见字符用可见字符表示。base64就是一种把不可见字符变成可见字符的编码方式。编码原理?基本原理 base64 将 ASCII 码 或者二进制编码成只包含 A~Z、a~z、0~9、+ 、/ 这64个字符(26个大写字符、26个小写字符、10个数字、+/)。这64个字符用6bit可以全部表示出来,剩下两个bit高位补零。编码规则3个8位字节一组,转换为4个6位字节,再在每个6位字节前面补两个0,构成4位8字节。base64将3字节转换为4字节,因此编码后的数据长度(以字节为单位),比编码前约多了1/3。如果编码前数据长度刚好是3的倍数,那么恰好多了1/3;如果不是,数据长度除以3的余数就是2或者1,转换的时候结果不足6位的,用0来填充,之后在6位前面补两个0,转换完空出的结果用 = 来补位,最后保证编码出来的字节为4的倍数。这里需要注意编码后的数据长度:编码前为3的倍数:len = strlen(str_in)/3 * 4编码前不是3的倍数:len = (strlen(str_in)/3 + 1 )* 4编码范围000000~111111:0~63base64 编码表码值字符码值字符码值字符码值字符0A16Q32g48w1B17R33h49x2C18S34i50y3D19T35j51z4E20U36k5205F21V37l5316G22W38m5427H23X39n5538I24Y40o5649J25Z41p57510K26a42q58611L27b43r59712M28c44s60813N29d45t61914O30e46u62+15P31f47v63/编码转换3字节字符,刚好 ABC -> QUJD 2字节字符,补一个 =(余数为1) AB -> QUI= 1字节字符,补两个 = (余数为2) A -> QQ==Base64开源库介绍项目地址:https://github.com/aklomp/base64.gitbase64_encodevoid base64_encode ( const char *src /* 编码前数据 */ , size_t srclen /* 编码前数据长度 */ , char *out /* 编码后数据 ,输出bufer 至少为输入的4/3 */ , size_t *outlen /* 编码后数据长度 */ , int flags /* 默认为0 */ ) ;base64_decodeint base64_decode ( const char *src /* 要解码的数据 */ , size_t srclen /* 要解码的数据长度 */ , char *out /* 解码后数据 ,输出bufer 至少为输入的4/3 */ , size_t *outlen /* 解码后数据长度*/ , int flags /* 默认为0*/ ) ;其他接口见项目源地址。注意在使用的时候需要注意编码前数据buffer的申请,如果出现编码后=后面出现多余字符。第一确保内存空间开辟正确,第二及时清理堆上脏数据。使用base64开源库,可以快速移植到不同的嵌入式平台上,同时还支持simd加速
zsh 是一个类似 bash 的 shell ,大多数 linux 系统默认使用 bash。本文简单介绍 zsh 安装及使用,同时推荐几个 zsh 的插件,以提高工作效率。zsh 安装安装 zshsudo apt-get install -y zshoh-my-zsh 安装安装 zsh 扩展集合:oh-my-zshsh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"注意,如果报 raw.githubusercontent.com|151.101.228.133|:443... 失败:拒绝连接“#wget 后面加 --no-check-certificate wget --no-check-certificate #或者安装ca-certificates app-get install ca-certificates -y #或者 apt-get install ssl-cert修改配置目前只安装使用以下插件:# ~/.oh-my-zsh/plugins/ git clone https://github.com/zsh-users/zsh-syntax-highlighting.git取消下划线: 自动补全或者提示:zsh-autosuggestionsgit clone https://github.com/zsh-users/zsh-autosuggestions智能提示: fzf 模糊搜索:oh-my-zsh默认支持。此插件一般用来快速定位目录。命令提示通过autosuggestions 即可自动记忆。修改配置文件:zsh 使用如果没有设置默认终端启动为zsh(有时在服务器上开发,不便粗暴的设置),启动终端,在终端输入zsh进入。退出时输入exit.总结如果你也使用zsh推荐安装oh-my-zsh。可以自定义自己的插件,提高工作效率。zsh-autosuggestions:自动补全提示已输入命令zsh-syntax-highlighting:检测命令输入错误fzf:可不用,根据个人习惯。
vimplus 介绍基本介绍vimplus 是一个开源的超强大的自动配置vim的工具。通过该工具可以快速定制、安装、卸载vim插件。从而实现快速配置vim.使用该工具的好处:自动化脚本一键安装;摈弃繁琐的环境配置;自动安装依赖;支持平台Mac OS XubuntuubuntuKylindebiankali linuxdeepinlinux mintelementarycenosfedoraarchlinuxmanjaroopensuse 安卓 64位(Termux)vimplus 安装目前仅在linux平台下安装,其他平台自行参考原项目说明。安装vimplus:git clone https://github.com/chxuan/vimplus.git ~/.vimplus cd ~/.vimplus ./install.sh设置Nerd Font:为防止vimplus显示乱码,需设置终端字体为Droid Sans Mono Nerd Font。多用户支持将vimplus在某个用户下安装好后,若需要在其他用户也能够使用vimplus,则执行sudo ./install_to_user.sh username1 username2 //替换为真实用户名更新vimplus:./update.shvimplus 配置~/.vimrc:为vimplus默认配置~/.vimrc.custom.plugins:用户自定义插件列表~/.vimrc.custom.config:为用户自定义配置文件。一般性配置可放入该文件,可覆盖~/.vimrc里面的配置插件列表插件说明备注cpp-mode提供生成函数实现、函数声明/实现跳转、.h .cpp切换等功能默认安装vim-edit方便的文本编辑插件默认安装change-colorscheme主题切换默认安装prepare-code新建文件时生成预定义代码默认安装vim-buffervim缓存操作默认安装vimplus-startifyvimplus开始页面默认安装,可不装tagbartaglist的替代品,显示类/方法/变量默认安装vim-plug比Vundle下载更快的插件管理软件默认安装YouCompleteMe史上最强大的基于语义的自动补全插件,支持C/C++、C#、Python、PHP等语默认安装,建议卸载,或者手动安装NerdTree代码资源管理器默认安装vim-nerdtree-syntax-highlightNerdTree文件类型高亮默认安装nerdtree-git-pluginNerdTree显示git状态默认安装vim-devicons显示文件类型图标默认安装,可卸载Airline可以取代powerline的状态栏美化插件默认安装auto-pairs自动补全引号、圆括号、花括号等默认安装LeaderF比ctrlp更强大的文件的模糊搜索工具默认安装ack强大的文本搜索工具默认安装vim-surround自动增加、替换配对符的插件vim-commentary快速注释代码插件默认安装vim-repeat重复上一次操作默认安装vim-endwiseif/end/endif/endfunction补全默认安装tabular代码、注释、表格对齐默认安装vim-easymotion强大的光标快速移动工具,强大到颠覆你的插件观默认安装incsearch.vim模糊字符搜索默认安装vim-fugitive集成 git默认安装gv显示 git 提交记录默认安装vim-slash优化搜索,移动光标后清除高亮默认安装echodoc补全函数时在命令栏显示函数签名默认安装vim-smooth-scroll让翻页更顺畅默认安装clever-f.vim强化f和F键默认安装vim-gutentagstags 标签生成插件,可自动生成tag自定义安装indentLine花括号对齐自定义安装mathjax-support-for-mkpmarkdown 支持mathjax公式自定义安装markdown-previewmarkdown 预览自定义安装快捷键快捷键说明备注,Leader Key可自定义<leader>n打开/关闭代码资源管理器常用<leader>t打开/关闭函数列表常用<leader>a.h .cpp 文件切换C++ 会用到<leader>u转到函数声明使用ctag 替代 个人习惯<leader>U转到函数实现使用ctag 替代<leader>u转到变量声明使用ctag 替代<leader>o打开include文件<leader>y拷贝函数声明<leader>p生成函数实现<leader>w单词跳转<leader>f搜索~目录下的文件,可修改默认搜索目录常用<leader>F搜索当前目录下的文本常用<leader>g显示git仓库提交记录<leader>G显示当前文件提交记录<leader>gg显示当前文件在某个commit下的完整内容<leader>ff语法错误自动修复(FixIt)<c-p>切换到上一个buffer常用<c-n>切换到下一个buffer常用<leader>d删除当前buffer常用<leader>D删除当前buffer外的所有buffer常用vim运行vim编辑器时,默认启动开始页面<F5>显示语法错误提示窗口<F9>显示上一主题<F10>显示下一主题<leader>l按竖线对齐<leader>=按等号对齐rr替换文本常用<leader>r全局替换,目前只支持单个文件常用rev翻转当前光标下的单词或使用V模式选择的文本gcc注释代码常用,可自定义gcap注释段落常用vif选中函数内容常用dif删除函数内容常用cif改写函数内容(选中并删除)常用vaf选中函数内容 (包括花括号, 函数名)常用daf删除函数内容 (包括花括号, 函数名)常用caf改写函数内容 (包括花括号, 函数名)常用fa查找字母a,然后再按f键查找下一个<leader>e快速编辑~/.vimrc文件<leader>s重新加载~/.vimrc文件<leader>vp快速编辑~/.vimrc.custom.plugins文件<leader>vc快速编辑~/.vimrc.custom.config文件<leader>h打开vimplus帮助文档<leader>H打开当前光标所在单词的vim帮助文档<leader>t生成try-catch代码块<leader><leader>y复制当前选中到系统剪切板常用<leader><leader>i安装插件<leader><leader>u更新插件<leader><leader>c删除插件相关配置.vimrc:默认插件 .vimrc.cunstom.plugins:扩展插件。根据自身需要。我这里安装了markdown相关的。.vimrc.cumtom.config:插件配置(分别配置gutentags自动生成tags,开启代码对齐线,以及默认使用google打开markdown预览)最终效果启动界面:目录窗口:其他效果(快速搜索,markdown预览,git记录,以及主题切换等),限于篇幅,暂不贴图。总结通过 vimplus 可快速定制自己的 vim。相比较传统的方式(vundle)安装配置起来更快(半小时搞定)。用户可以根据需要修改适合自己的快捷键。如果你,不想折腾,可以一试!相关阅读,见旧文vundle配置vim!
说明:本文只做编译环境构建,对ffmpeg使用不作过多介绍写本文的目的:学会在Windows编译ffmpeg源码。尽管官方已有编好的库鉴于大多数是基于Linux 构建的。同时Windows构建过程比较复杂,各种依赖。希望写一个简单的教程,以方便后续做定制话处理 。准备环境windows10 64msys2ffmpeg4.2.2 源码Qt 5.14.2(随便一个Qt5版本就行,我用的最新版本测试,也可以在msys2安装qt)软件安装安装 msys2 (https://www.msys2.org/)msys2 是Windows软件发行版和构建平台,它的核心是基于现代Cygwin(POSIX兼容层)和MinGW-w64的MSYS的独立重写,目的是更好地与本机Windows软件互操作。它提供了一个bash shell,Autotools,版本控制系统等,用于使用MinGW-w64工具链构建本机Windows应用程序。简单来说,就是在Windows上模拟Linux运行环境,用来构建Windows应用程序以及运行库(dll)安装教程见官网(https://www.msys2.org/)修改软件源:默认下载很慢# 打开 c:\msys64\etc\pacman.d\mirrorlist.mingw32 # 在前面加上如下: Server= https://mirrors.tuna.tsinghua.edu.cn/msys2/mingw/i686/ # 打开 C:\msys64\etc\pacman.d\mirrorlist.mingw64 # 在前面加上如下: Server=https://mirrors.tuna.tsinghua.edu.cn/msys2/mingw/x86_64/ # 打开C:\msys64\etc\pacman.d\mirrorlist.msys # 在前面加上如下: Server= https://mirrors.tuna.tsinghua.edu.cn/msys2/msys/$arch/更新软件包数据库和核心系统软件包(打开msys2 msys终端)#建议首次全安装,点yes pacman -Syu普通msys2软件包pacman -S make pkg-conf diffutils mingw-w64软件包和工具链(这里以mingw-w64为例)运行 msys2_shell.cmd -mingw64(或者运行MSYS2 MinGW 64-bit) 打开mingw64终端# nasm 汇编 #gcc mingw-w64 ffmpeg工具链 #sdl2 ffmpeg依赖 pacman -S mingw-w64-x86_64-nasm mingw-w64-x86_64-gcc mingw-w64-x86_64-SDL2 #gcc 工具链也可通过一项命令安装。我是通过上面命令单独安装的 pacman -S mingw-w64-x86_64-toolchain #mingw32 32位的依赖库也要统一 pacman -S mingw-w64-i686-gcc 或者pacman -S mingw-w64-i686-toolchain其他# 安装git(下载源码) pacman -S git # 安装 vim(可不装) pacman -S vim # 如需要 pacman -S base-devel 至此ffmpeg基本环境已构建完成:工具链:mingw-w64-x86_64-gccmakeSDL2:这里只添加 了sdl2的库,其他依赖暂未加入git:同步代码安装qt5可以通过msys2安装,也可在qt官网自行下载。笔者之前安装过最新qt,直接用就可以。# msys2 安装 pacman -S mingw-w64-x86_64-qt5 mingw-w64-x86_64-qt-creator源码编译打开mingw64 终端切换到ffmpeg源码目录:cd /c/OpenSource/ffmpeg配置# 配置 生成共享库及安装路径 # 这里注意,在mingw64会自动检测工具链。如果有多个工具链别弄错了。 # 最好设置环境变量 ./configure --enable-shared --prefix=/c/OpenSource/ffmpeg/out错误的配置工具链正确的配置编译及安装make; make installbin目录lib 目录include 目录测试建立Qt 工程修改 .pro 包含头文件测试 打印版本信息和配置注意:运行自己编的应用程序需要将ffmpeg的dll复制到exe目录下。包括SDL2.dll总结网上大多数教程是基于visual studio。而且依赖的第三方库比较多,初学容易困惑。安装msys2,构建unix编译环境(make,cmake,gcc等),一样的操作流程。无需过多的修改一些配置文件,摒弃msvc的一些东西。疑问:我们windows上的dll文件是如何生成的呢?既然模拟unix运行环境,生成的应该是.so和.a才对。事实上编译过程中已经给出答案了。通过dlltool 将*.def转换为*.lib和*.dll举一反三:其他开源的跨平台的库,是否都可以通过msys2来构建对应的环境生成windows所需的库?不用每次去安装msvc了。比如sqlite等参考1.ffmpeg 编译指南:https://trac.ffmpeg.org/wiki/CompilationGuide2.wikiCompilationGuide/MinGW:https://trac.ffmpeg.org/wiki/CompilationGuide/MinGW3. ffmpeg 官方文档:http://ffmpeg.org/platform.html#Windows
Tufao 介绍Tufao 是在Qt 之上构建的 C++异步Web框架。项目地址来源于github(https://github.com/vinipsmaker/tufao)利用Qt 的对象通信系统(信号与槽机制),可以快速的迁移到Qt中。具有以下特点:高性能独立服务器跨平台支持HTTP 支持HTTPS 支持灵活请求路由器静态文件服务器灵活安全的会话支持支持QtCreator 可以快速创建新的应用程序超时支持C++ 11安装部署目前官网最新版本在1.4.5。注意从1.4.0开始,tufao引入第三方库使用boost.http进行解析。故下载最新源码前需提前安装配置boost库.本文以1.3.10源码为例,介绍如何安装部署tufao到自己的项目中。最新版本编译及解决方法下篇文章介绍。准备环境windwos 10 64位Qt 5.8.0 MinGw 32下载源码访问如图所示github链接,找到如图所示1.3.10版本源码,下载下来。也可以从后台回复 tufao-1.3.10 下载下载CMake安装Cmake,并设置环境变量。官网地址 https://cmake.org/download/设置好环境变量C:\Qt\Qt5.8.0\5.8\mingw53_32\binC:\Qt\Qt5.8.0\Tools\mingw530_32\binC:\Program Files\CMake\bin配置Cmake编译环境解压tufao-1.3.10 源码,并在里面创建一个build目录,目录下创建debug/release目录配置Cmake如果报错,请检查如下配置4tufao编译配置编译类型安装路径 点击Configure 和 Generate编译和安装tufao打开Qt 5.8 for Desktop (MinGw 5.3.0 32 bit)cd 先前配置的debug目录下。(cd C:\OpenSource\tufao-1.3.10\build\debug)mingw32-makemingw32-make install安装过程注意做了两件事:将include 和 lib复制到 out目录下;将 C:\OpenSource\tufao-1.3.10\build\debug\pkg\tufao1.prf 复制到C:/Qt/Qt5.8.0/5.8/mingw53_32/mkspecs/features/tufao1.prf 下;可以看到prf里面自动帮我们链接的tufao1库。/*tufao1.prf*/ QT += network DEFINES += TUFAO_VERSION_MAJOR=1 INCLUDEPATH += "C:/OpenSource/tufao-1.3.10/build/out/include/tufao-1" win32 { CONFIG(debug, debug|release): LIBS += -L"C:/OpenSource/tufao-1.3.10/build/out/lib"-ltufao1d CONFIG(release, debug|release): LIBS += -L"C:/OpenSource/tufao-1.3.10/build/out/lib"-ltufao1 } else{ LIBS += -L"C:/OpenSource/tufao-1.3.10/build/out/lib"-ltufao1 }使用只需要在pro文件加入 CONFIG+= C++11 TUFAO1即可测试打开 C:\OpenSource\tufao-1.3.10\examples\qmake\hello-world 工程,运行应用程序,并使用postman发送get请求。测试结果如下。最后感谢 vinipsmaker 提供的开源库,本文介绍内容仅作学习参考,不作它图,如需相关源码请前往原作者github取,或者后台回复 tufao-1.3.10 下载相关配置环境。
环境内核版本:linux 3.10.14busybox版本:v1.22.1开发板:dhcp 客户端路由器:dhcp 服务器配置内核make menuconfig[*] Networking support ---> Networking options ---> <*> Packet socket <*> Unix domain sockets [*] TCP/IP networking [*] IP: kernel level autoconfiguration [*] IP: DHCP support [*] Network packet filtering framework (Netfilter) --->配置busyboxmake menuconfigNetworking Utilities ---> [*] udhcp client (udhcpc) [*] udhcp server (udhcpd) (dhcp 服务器可以不配)建立配置文件cd /busybox/examples/udhcp/cp simple.script /usr/share/default.script(/usr/share/开发板的路径)/usr/share/default.script为 udhcpc运行处理的默认脚本运行开机自启:在/etc/init.d/rcS 添加udhcpc命令(/sbin/udhcpc )手动运行:上电,在板子运行后直接执行udhcpc命令其他使用dhcp服务时需要一个dhcp服务器,一般是板子做客户端,路由器做dhcp服务器,自动给板子分配IP。udhcpc 如果检测不到dhcp服务器会一直“Sending discover...” 导致程序阻塞,可结合udhcpc命令做一些定制化处理dhcp 租约到期后,需要重新续租,否则IP可能会被其他设备使用。同时租约到期后会有短暂的网络离线。Usage: udhcpc [-fbqvaRB] [-t N] [-T SEC] [-A SEC/-n] [-i IFACE] [-s PROG] [-p PIDFILE] [-oC] [-r IP] [-V VENDOR] [-F NAME] [-x OPT:VAL]... [-O OPT]...常用参数:udhcp -i eth0udhcp -i eth0 -s ./default.script(指定dhcp处理文件)udhcp -q -i eth0 -s ./default.script(获取IP后退出dhcpc)udhcp -n -i eth0 -s ./default.script(获取失败退出dhcpc)udhcp -nq -i eth0 -t 6 -s ./default.script(重复发送6次)udhcpc -b -i eth0 -p /var/run/udhcpc.pid -R 后台运行(-R 退出租约)udhcpc -r 重新续租
创建项目创建一个QtDomXmlTest的widget项目。Qt如何创建项目,网上有很多教程,这里就不介绍了。修改.pro文件,增加xml支持读写操作为了方便管理,这里自己单独创建了一个操作xml的基类,所有的读写接口都在基类里实现,以后也可以扩展。注意:这里需要包含头文件QtXml创建xml文件QDomDocument:doc文档QDomProcessingInstruction:xml文档说明QDomElement:xml元素/** * @funtion createDomXml * @brief create xml * @param strRoot Root 节点 * @return no */ bool DomXmlBase::createDomXml(QString strRoot) { QDomDocument doc; // 添加处理指令即XML说明 QDomProcessingInstruction instruction; instruction = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""); doc.appendChild(instruction); // 添加根元素 QDomElement root = doc.createElement(strRoot); doc.appendChild(root); QFile file(m_strFileName); if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } QTextStream out(&file); // 将文档保存到文件,4为子元素缩进字符数 doc.save(out, 4); file.close(); return true; }通过doc对象创建一个xml文件同时包含一个根节点stroot。注:这里的m_strFileName是该类的私有变量,通过构造时赋值的 插入一个元素 /** * @funcname addDomXml * @brief 插入一个节点 * @param strId 序号 * @param strElementListText <元素值> * @return void * @example * <Peoples> * <People Id="001"> * <Name></Name> * <Age></Age> * <Gender></Gender> * <Hobby></Hobby> * .... * </People> * </Peoples> */ void DomXmlBase::addDomXml(QString strId, QList<QString> strElementListText) { if(strElementListText.isEmpty()) { qDebug() << "strElementListText is empty"; return; } /*1. open file*/ QFile file(m_strFileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; QDomDocument doc; if (!doc.setContent(&file)) { qDebug() << "doc.setContent failed"; file.close(); return; } file.close(); /*2. add element*/ QDomElement root = doc.documentElement(); if(m_RootElement.isEmpty()) return; /*root节点下第一元素*/ QDomElement device = doc.createElement(m_RootElement); /*为节点赋属性*/ QDomAttr id = doc.createAttribute("Id"); QList<QDomElement> domElementList; id.setValue(strId); device.setAttributeNode(id); int count = strElementListText.count(); int Elementcount = m_ElementList.count(); if(count != Elementcount) { qDebug() << "text count != Elementcount"; QMessageBox::critical(NULL,"注意","text count != Elementcount"); return ; } for (int i = 0; i < count; ++i) { QString strElementkey = m_ElementList.at(i); QString strElementvalue = strElementListText.at(i); QDomElement domElement = doc.createElement(strElementkey); QDomText text; text = doc.createTextNode(strElementvalue); domElement.appendChild(text); device.appendChild(domElement); } root.appendChild(device); /*3.save xml*/ if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return ; } QTextStream out(&file); doc.save(out, 4); file.close(); qDebug() << "add Element xml"; }m_RootElement:根节点下第一元素m ElementList:m RootElement的所有子元素这两个值是在父类构造函数调用setXmlElement初始化的,设计这两个成员变量的初衷是为了让这个类适应同类型结构的所有xml,可以任意修改元素标签以及增加元素的个数。 /** * @funcname setXmlElement * @brief 设置根元素标签,元素名称,返回根元素下个数。需先调用 * @param strRootElement * @param strList * @return count */ int DomXmlBase::setXmlElement(QString strRootElement, QList<QString> strList) { m_ElementList = strList; m_RootElement = strRootElement; return strList.count(); }具体用法如下: void Widget::on_btnAdd_clicked() { DomXmlBase xml("info.xml"); QList<QString> strList; strList << "Name" << "Age" << "Gender" << "Hobby"; xml.setXmlElement("people",strList); QList<QString> strListText; strListText << "Tom" << "22" << tr("男") << tr("跑步"); xml.addDomXml("001",strListText); }或者 删除一条记录这里是按进行ID查找的,如果检测到id相同则,删除。/** * @funcname deleteDomXml * @brief delete a Element * @param strId * @return no */ void DomXmlBase::deleteDomXml(QString strId) { QFile file(m_strFileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return ; QDomDocument doc; if (!doc.setContent(&file)) { file.close(); return ; } file.close(); // 以标签名进行查找 if(m_RootElement.isEmpty()) return; QDomNodeList list = doc.elementsByTagName(m_RootElement); for(int i=0; i<list.count(); i++) { QDomElement e = list.at(i).toElement(); if(e.attribute("Id") == strId) //如果相等测更新或者删除元素值 { // 如果元素的“编号”属性值与我们所查的相同 // 如果是删除操作 QDomElement root = doc.documentElement(); // 从根节点上删除该节点 root.removeChild(list.at(i)); QFile file(m_strFileName); if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) return ; QTextStream out(&file); doc.save(out,4); file.close(); qDebug() << "delete Element xml"; } } }修改一条记录这里是修改全部子元素的记录,基本上和添加相似,主要熟悉一下接口的使用。/** * @funcname updateDomXml * @brief update Element * @param strId * @param strListText * @return no */ void DomXmlBase::updateDomXml(QString strId, QList<QString> strListText) { if(strListText.count()<m_ElementList.count()) { QMessageBox::critical(NULL,"注意","strListText个数小于m_ElementList"); return ; } QFile file(m_strFileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return ; QDomDocument doc; if (!doc.setContent(&file)) { file.close(); return ; } file.close(); // 以标签名进行查找 if(m_RootElement.isEmpty()) return; QDomNodeList list = doc.elementsByTagName(m_RootElement); for(int i=0; i<list.count(); i++) { QDomElement e = list.at(i).toElement(); if(e.attribute("Id") == strId) { // 如果元素的“编号”属性值与我们所查的相同 QDomNodeList child = list.at(i).childNodes(); for(int j = 0; j < child.count(); j++) { QString strText = child.at(j).toElement().text(); QString strSetValue = strListText.at(j); if(strText != strSetValue) { child.at(j).toElement().firstChild().setNodeValue(strSetValue); } } QFile file(m_strFileName); if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) return ; QTextStream out(&file); doc.save(out,4); file.close(); qDebug() << "update Element xml"; break; } } }读一条记录 /** * @funcname readDomXml * @brief 根据id获取其下元素值 * @param strId * @param strListText output param * @return no */ void DomXmlBase::readDomXml(QString strId, QList<QString> &strListText) { QFile file(m_strFileName); if(!strListText.isEmpty()) strListText.clear(); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return ; QDomDocument doc; if (!doc.setContent(&file)) { file.close(); return ; } file.close(); // 以标签名进行查找 if(m_RootElement.isEmpty()){ QMessageBox::critical(NULL,"注意","请先设置setXmlElement"); return; } QDomNodeList list = doc.elementsByTagName(m_RootElement); for(int i=0; i<list.count(); i++) { QDomElement e = list.at(i).toElement(); if(e.attribute("Id") == strId) { // 如果元素的“编号”属性值与我们所查的相同 QDomNodeList child = list.at(i).childNodes(); for(int j = 0;j < child.count(); j++) { QString strText; strText = child.at(j).toElement().text(); strListText.append(strText); } break; } } }可以看到readDomXml,updateDomXml,deleteDomXml操作基本相似,几个接口输入输出差异也不大,可以继续封装,根据操作进行操作,最小范围内暴露出一些接口。doXml(QString operate,QString strId,QList<QString> strListText);总结本文主要介绍了QT通过dom操作xml,主要是一些二进制代码实现,希望读者能够通过此文,熟悉xml操作,并且可以自行封装自己想要的接口。不足之处:如果不在父类构造函数初始化setXmlElement,每次创建基类对象后,都需要手动setXmlElement,否则查询时,找不到元素标签。
I2C时序简单分析I2C总线介绍起始信号:SCL位于高饱和时,SDA由高->低,即下降沿时停止信号:SCL处于高饱和时,SDA由低->高,,即上升沿传输数据:SCL为低可以改变SDA数据上的替换。在SCL上升沿的过程将SDA数据传输出去应答:I2C以字节(8位)的方式进行传输,总线上每传输完成一个字节后会产生一个应答信号,master需要产生对应的一个额外时钟。(SDA为应答表示应答,为高认可表示没有应答)正常的I2C总线数据:开始+ I2C设备ID + R / W + ACK +数据(0)+ ACK + ... + ... +数据(n)+ ACK + STOP波形图分析写 主机写,从机应答,主机读取应答:写完一字节(8位)后,读取从机应答位:若为0,表示从机应答,可以继续下一步操作,若为1,表示从机非应答,不能进行下一步操作。 如图(红色是SDA):发送一个字节”0”。首先产生开始信号,然后依次发送7位设备地址(0101010)+ W(0)+ ACK(0)+ 8bitdata(00110000)+ ACK(0 )+停止信号读主机读,主机产生应答:读完一字节(8位)后,由主机产生应答(或非应答位):若产生应答位,表示可以继续读下一个字节,若产生非应答,表示不可以继续读下一字节操作。如图(红色是SDA):读一个字节“ 0”。首先产生开始信号,然后依次是7位设备地址(0101010)+ W(1)+ ACK(0)。+数据(00110000)+ NACK(1)+停止信号读取多个字节如图(红色是SDA):读2个字节“ 01”。首先产生开始信号,然后依次是7位设备地址(0101010)+ W(1)+ ACK(0)+ data0(00110000)+ ACK( 0)data1(00110001)+ NACK(1)+停止信号
安装pandoc什么是pandocpandoc是一个文件转换工具,支持不同文件间的相互转换,具体见https://github.com/jgm/pandoc 输入文件,支持的格式如下:commonmark creole docbook docx dokuwiki epub fb2 gfm haddock html ipynb jats json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mediawiki muse native odt opml org rst t2t textile tikiwiki twiki vimwiki输出文件支持格式如下:asciidoc asciidoctor beamer commonmark context docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mediawiki ms muse native odt opendocument opml org plain pptx revealjs rst rtf s5 slideous slidy tei texinfo textile xwiki zimwiki安装参考:https://pandoc.org/installing.html。进入下载页面,获取安装包#linux #$TGz源文件路径 $DEST安装路径 tar xvzf $TGZ--strip-components 1 -C $DEST修改配置修改.gitconfig1.添加文件名中文支持 [core] quotepath = false 2. wdiff比较word [ diff "pandoc"] textconv=pandoc --to=markdown prompt = false [alias] wdiff = diff --word-diff=color --unified=1增加.gitattributes你可以在你项目的文件根目录下里新增一个.gitattributes文件,并进行设置。如果不想让这些属性文件与其他文件一同提交,也可以在.git/info/attributes文件进行设置#.git/info/ attributes *.docx = pandoc如果想要查看历史修改:git log -p --word-diff=color file.docx不足1. 不支持doc读取,只能比较docx文件,如果要对文档进行管理,建议保存为docx2. 只能比较显示的文本,对图片增加删除修改后,比较显示的是 markdown格式,不利于观看3. 总的来说,如果要管理文档,还是建议先用其他工具比较在提交比较靠谱.git diff对比有些不足。4. 如有其他实现手段欢迎后台留言讨论。
bootloader基本介绍1.什么是bootloader?bootloader 简单来说就是引导加载程序。即在操作系统内核运行之前运行一小段程序。通过bootloader,引导启动内核操作系统。通常 bootloader 严重依赖于硬件的实现,特别是在嵌入式的世界。2.bootloader的作用设置系统时钟、初始化SDRAM;对 bootloader 阶段所需要用到的硬件资源进行初始化;设置启动参数,启动内核。3.bootloader的启动过程通常 bootloader 启动过程可分为单阶段和多阶段。以uboot(支持ARM和MIPS)为例,通常启动过程分为两阶段,即stage1和stage2。stage1:第一阶段第一阶段通常是用汇编写的,依赖于硬件的体系结构,通常是一些设备的初始化硬件设备初始化(串口、时钟、SDRAM等)为 bootloader 的 stage2 准备 RAM 空间拷贝 bootloader 的 stage2 到 RAM设置好栈跳转到 stage2 的 C 语言入口点执行stage2:第二阶段第二阶段通常是 C 语言写的,主要是为了方便移植,通常包含以下步骤:初始化本阶段要使用的硬件设备检测系统内核映射将kernel映像和根文件系统映像从 flash 上读到 RAM 空间上为内核设置启动参数启动内核Linux kernel 认识Linux kernel 是整个Linux操作系统的核心,主要功能就是负责进程管理调度、内存管理、虚拟文件系统(VFS)、网络管理、进程间通信(IPC)如下图是 Linux 内核的整体架构。根据内核的核心内容,linux 内核提出了5个子系统,分别负责如下功能:Process Scheduler:也称作进程管理、进程调度。负责管理CPU资源,让每个进程尽可能公平的访问CPU。Memory Manager:内存管理。负责管理内存资源,以便让各个进程可以安全的共享机器的内存资源。另外内存管理还会提供虚拟内存机制,可以让进程使用多于 Memory 的内存,不用的内存会通过文件系统保存在外部非易失存储器中,需要使用的时候,再取回到内存中。VFS(Virtual File System ):虚拟文件系统。Linux内核将不同功能的外部设备,例如Disk设备(硬盘、磁盘、NAND Flash、Nor Flash等)、输入输出设备、显示设备等等,抽象为可以通过统一的文件操作接口(open、close、read、write等)来访问。这就是Linux系统“一切皆是文件”的体现。Network:网络子系统。负责管理系统的网络设备,并实现多种多样的网络标准。IPC (Inter-Process Communication):进程间通信。IPC不管理任何的硬件,它主要负责Linux系统中进程之间的通信。rootfs:根文件系统Linux 系统中的根文件系统(Root File System),简称rootfs,本质上还是一个文件系统。和普通文件系统不同的是,rootfs 是针对特定的操作系统的架构的一种实现形式。具体表现为:特定的文件夹,文件夹之间的关系,即组织架构或者目录结构特定的文件Linux 系统中 rootfs 通常包含以下文件夹和文件:/:根文件目录/etc:相关的配置文件目录/etc/init:存放系统启动相关的配置/sbin:存放系统相关工具,系统命令/bin:存放常用的一般命令等一个根文件系统,主要存放以下内容:相关配置文件系统相关应用程序一般应用程序可执行程序所需要的环境等bootloader、kernel 和 rootfs 在嵌入式系统中的分区结构通常,当我们代码编写调试完成后,就需要通过一些工具或者命令来制作固件(这里的固件不作详解),然后将固件烧录到芯片上,固化在特定的硬件设备上,这个时候,就需要了解boot loader、kernel 和 rootfs的分区组织。可以看到,我们的应用程序,最终是放在在文件系统上执行的。总结 一个完整嵌入式系统执行:bootloader引导启动内核;内核挂在根文件系统和普通文件系统;在文件系统下执行特定的应用程序。
1. 目录与路径目录的相关操作几个特殊的目录:. :表示当前目录.. :表示上一层目录- :表示前一个工作目录~ :表示当前用户所在的主文件夹~account :表示account用户所在的主文件夹几个常见的处理目录命令cd:切换目录cd dir例如:cd /home 或者 cd ../pwd:打印当前目录的全路径pwd [-p]-p 参数表示显示正确的路径而不是连接文件的路径。mkdir:新建一个目录mkdir [-mp]m :配置文件权限,直接设置,不需要看默认权限 -p: 递归创建,包括子目录mkdir -p test1/test2/test3(递归创建)mkdir -m 711 test2(设置文件权限)rmdir:删除一个空的目录rmdir [-p]-p 参数连同上一层的空目录一起删除,递归删除 rmdir -p test1/test2/test3执行文件路径的变量:$PATH PATH表示环境变量,内容是由一堆目录构成,每个目录中间用冒号(:)来分隔,并且有顺序之分。处于环境变量中的一些重要执行文件可以在系统任意处执行。2. 文件与目录管理查看文件与目录:ls 参数如下:-a:全部文件。(常用) -A:全部文件,但不包括.和.. -d:仅列出目录本身。(常用) -f:直接列出结果,不排序(ls 默认会以文件名排序) -F:根据文件、目录等信息给予附加信息。 * :表示可执行文件;/:表示目录; = :表示socket文件; | :表示FIFO-h :将文件容量以GB、KB等列出来-l:列出文件详细信息,包括文件类型、权限、修改信息等-r:将排序结果反向输出 -S:以文件容量大小排序 -t:以时间排序 –full-time:以完整时间显示复制、删除与移动:cp、rm、mvcp :复制cp [-adfilprus] 源文件 目标文件-d:若文件为连接文件属性(link file),则复制连接文件属性而非文件本身-f:为强制复制,若目标文件已经存在且无法开启,则先删除在复制-i:若目标文件已存在,则会先询问是否覆盖,在操作-l:进行硬链接的文件创建,而非文件本身-p:连同文件属性一同复制-r:递归持续复制,用于目录的复制行为-s:复制成为符号连接文件,即”快捷方式“-u:若目标文件比源文件旧才更新rm:删除rm [-fir] 文件或目录-f:强制删除-i:在删除前提示用户-r:递归删除mv:移动mv [fiu] 源文件/目录 目标目录mv [options] 源1 源2 源3 … 目标(将多个源移动到目标目录)-f :强制移动-i: 移动前询问,防止覆盖-u:若目标已经存在,且源比较新才更新取得路径的文件名与目录名称basename:取得最后一个文件名basename /etc/sysconfig/network得到文件名:networkdirname:取得目录名dirname /etc/sysconfig/network得到目录名:/etc/sysconfig3. 文件内容查找常用查找命令如下:cat:由第一行开始显示文件内容 tac:从最后一行开始显示 nl: 显示的时候输出行号 more:一页一页的显示内容 less:与more类似,同时可以翻页 head:只看头几行 tail:只看结尾几行 od:以二进制方式读取文件内容直接查看文件内容catcat [-AbEnTv] 文件名-A:相当于 -vET 的组合参数,可列出一些特殊字符,而不是空白而已 -b:列出行号,仅针对非空白行做行号显示 -E:将结尾的断行字符 $ 显示出来 -n:打印出行号,连同空白行号也打印出来 -T:将tab键以 ^I 显示出来 -v:列出一些看不出来的特殊字符tactac 文件名nlnl [-bnw] 文件 -b:指定行号显示方式-b a:同cat -n,显示所有行号-b t:不列出空白行行号,默认值 -n:列出行号表示方法-ln:最左方显示行号-rn:【自己字段】的最右方显示行号,且不加0-rz:行号在【自己字段】的最右方显示,且加0 -w:行号字段占用位数翻页查看more(一页一页翻动)more /etc/manpatch.config 最后一行显示文件的百分比。在more运行过程中,可以通过以下几个按键进行控制:空格键(space):向下翻一页Enter:向下滚动一行/字符串:查找字符串:f:显示出文件名以及目前显示的行数q:离开moreb或者Ctrl + b :往回翻页,不过这操作只对文件有用,对管道无用less(一页一页翻动)less /etc/manpatch.config less用法比more更加灵活,可以向前向后翻页;向前向后查找:空格键:向下翻页Pagedown:向下翻页Pageup:向上翻页/字符串:向下查找?字符串:向上查找n:重复前一个查询N:反向重复前一个查询q:离开less这个程序 数据选取head(文件开头)head [-n number] 文件 (-n 后接数字,代表显示几行的意思) 默认显示前10行tail(显示后面的几行数据)tail [-n number]非纯文本文件读取:odod [-t TYPE] 文件 -t :后面接文件类型a:默认字符输出c:使用ASCII字符输出d[size]:利用十进制输出,每个整数占size bytesf[size]:利用浮点数输出,每个数占size byteso[size]:利用八进制输出,每个数占size bytesx[size]:利用十六进制输出,每个数占size bytes4. 修改文件时间和创建文件:touchlinux下会记录三个主要变动的时间:modification time(mtime)、status time(ctime)、access time(atime)modification time(mtime):当文件内容更改时,更新该时间status time(ctime):当文件状态改变时,更新该时间access time:当文件被取用时,就会更新这个读取时间文件的时间是很重要的,通过touch命令可以修改文件时间。touch [-acdmt] 文件 -a :修改访问时间 -c: 仅修改文件的时间,若文件不存在,则不创建新文件 -d: 后面可接修改的日期,而不用目前的日期,也可以使用–data==”日期或时间” -m:仅修改mtime -t: 后面可接修改的时间,而不用目前的时间,格式为[YYMMDDhhmm]5. 文件与目录的默认属性和隐藏属性文件默认权限:umaskumask就是指定“目前用户在新建文件或目录时候的权限默认值”。 查询umask默认值:umask: 显示的数值后三位与一般权限有关。其数值刚好与文件属性相反,为1表示屏蔽属性。 umask -S: 显示的是文件属性。 umask 数字:设置默认属性 文件隐藏属性:chattr,lsattrchattr:设置文件的隐藏属性(只在Ext2/Ext3的文件系统上有效chattr [+-=] [ASacdistu] 文件或目录名 +:增加某一特殊参数 -: 删除某一特殊参数 =:仅有后面接的参数 A:设置A属性后,他的访问时间atime不会被修改 S:一般文件是异步写入磁盘的,加入S属性后,会“同步写入磁盘 a:设置a后,这个文件只能增加数据,不能删除和修改数据,只有root才能设置这个属性 c:设置c后,将会自动将此文件压缩,读取时自动解压。 d:当dump程序执行时,设置d属性可使该文件和目录不会被dump备份 i: 让一个文件“不能被删除,改名,设置连接也无法写入或添加数据”,只有root能设置此属性 s:当设置s后,如果删除一个文件,则该文件会被完全的从这个硬盘空间删除 u:与s相反。删除的文件可以从磁盘恢复lsattr(显示文件的隐藏属性)lsattr [-adR] 文件或目录 -a :将隐藏的文件属性显示出来 -d: 如果接的是目录,仅列出目录本身属性 -R: 连同子目录数据也一并列出来查看文件类型:filefile 文件名6. 命令和文件查询脚本文件名的查询which(寻找“执行文件”)which [-a] command -a :将所有由PATH目录中可以找到的命令均列出,而不只是第一个被找到的命令文件名的查找whereis(寻找特定文件)whereis [-bmus] 文件或目录名 -b: 查找二进制 -m:只在manual路径下查找 -s: 只在source源文件查找 -u: 查找不在上述三个选项当中的其他特殊文件locate(找出相关的文件)locate [-ir] keyword -i :忽略大小写的差异 -r:后面可接正则表达式 locate passwd :列出所有与passwd相关的文件名 注意:这个locate寻找的数据是经由数据库/var/lib/mlocate/里面的数据查找的,所以不用再去硬盘里访问数据,速度很快。但是数据库的创建,默认每天执行一次,如果新建新的文件后必须手动更新数据库(updatebd),否则会找不到。 updatebd:根据/etc/updatedb.conf的设置去查找系统硬盘内的文件名,并更新var/lib/mlocate内的数据库文件findfind [PATH] [option] [action]与时间有关的参数:-atime,-mtime,-ctime-mtime +n :列出n天之前的文件-mtime -n : 列出n天之内包含n的文件-newer file: 列出比file还要新的文件与用户或用户组有关的参数:-uid n: n为数字,是用户账号的ID,即UID-gid n :n为数字,是用户组的ID,GIDuser namegroup name与文件权限及名称有关的参数:-name filename :查找文件名为filename-size [+-]SIZE:查找比SIZE还要大(+)或者小(-)的文件type TYPE :查找文件类型(f:常规文件;b,c设备文件;d:目录;l:连接文件;s:socket;p:FIFO)perm mode : 查找文件权限等于mode的文件 -perm -mode : 必须全部包含mode权限(三个权限) -perm +mode : 任意一个包含mode权限即可 find -perm +7000:只要包含s或t即可 find -perm -7000: 必须包含—s–s–t所有的三个权限7. 权限与命令之间关系用户进入一个目录成为可工作目录的基本权限可使用的命令:如cd等目录所需要的权限:用户对这个目录至少需要具有x权限。额外需求:如果用户想要在某个目录使用ls查询,还需要具备r权限用户在某个目录读取一个文件的基本权限可使用的命令:如cat,more,less等目录所需要的权限:用户对这个目录至少需要具有x权限。文件所需权限:用户对文件至少要具有r权限用户在某个目录修改一个文件的基本权限可使用的命令:如nano,vi等目录所需要的权限:用户对这个目录至少需要具有x权限。文件所需权限:用户对文件至少要具有r,w权限 用户在创建一个文件的基本权限目录所需要的权限:用户对这个目录至少需要具有w,x权限。 用户进入目录并执行该目录下的某个命令的基本权限目录所需要的权限:用户对这个目录至少需要具有x权限。文件所需权限:用户对文件至少要具有x权限
2023年05月