-
1. 红外线知识点介绍在光谱中波长自760nm至400um的电磁波称为红外线,它是一种不可见光。目前几乎所有的视频和音频设备都可以通过红外遥控的方式进行遥控,比如电视机、空调、影碟机等,都可以见到红外遥控的影子。这种技术应用广泛,相应的应用器件都十分廉价,因此红外遥控是我们日常设备控制的理想方式。红外线遥控是目前使用最广泛的一种通信和遥控手段。由于红外线遥控装置具有体积小、功耗低、功能强、 成本低等特点,因而,继彩电、录像机之后,在录音机、音响设备、空凋机以及玩具等其它小型电器装置上也纷 纷采用红外线遥控。工业设备中,在高压、辐射、有毒气体、粉尘等环境下,采用红外线遥控不仅完全可靠而且 能有效地隔离电气干扰。NEC协议是众多红外线协议中的一种,以前广泛用在电视机,投影仪设备里,很早之前经常说的万能电视遥控器就是NEC协议的。当前文章就介绍如何在Linux下通过红外线接收模块,编写一个NEC协议的红外线解码驱动,解析遥控器传输过来的各种控制指令,完成对应的动作响应;驱动里用到了外部中断接收数据,通过定时器计算间隔时间完成解码。NEC协议的特点如下:单个码一共分为5各部分(没有算重复码): 引导码+用户码+用户反码+按键码+按键反码对于接收方_引导码: 9ms的低电平+4.5ms的高电平。接收的数据是0: 560us低电平+560us高电平接收的数据是1: 560us低电平+1680us高电平2. 硬件环境当前开发板采用友善之臂的Tiny4412,CPU是三星的EXYNOS4412,最高主频为1.5GHZ,Linux内核版本是3.5。下面是红外线接收模块原理图:通过杜邦线接在开发板的中断输入脚上: (GPX1_0接口上,第9个排针。)驱动安装后,解码的效果:3. 案例代码驱动代码思路: 采用外部中断接收NEC的数据,在工作队列里完成协议解析,最终通过printk打印出来。#include <linux/kernel.h> #include <linux/module.h> #include <linux/interrupt.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/delay.h> #include <linux/workqueue.h> static int irq; #define NEC_INFRARED_GPIO EXYNOS4_GPX1(0) static struct work_struct work; /*获取高电平持续时间--us单位*/ static u32 GetTimeH(void) { ktime_t my_time,my_time2; unsigned int i,j; my_time=ktime_get(); //获取当前时间 i=ktime_to_us(my_time); //转 us while(gpio_get_value(NEC_INFRARED_GPIO)){} my_time2=ktime_get(); //获取当前时间 j=ktime_to_us(my_time2); //转 us return j-i; } /*获取低电平持续时间--us单位*/ static u32 GetTimeL(void) { ktime_t my_time,my_time2; unsigned int i,j; my_time=ktime_get(); //获取当前时间 i=ktime_to_us(my_time); //转 us while(gpio_get_value(NEC_INFRARED_GPIO)==0){} my_time2=ktime_get(); //获取当前时间 j=ktime_to_us(my_time2); //转 us return j-i; } /* 工作函数 */ static u8 buf[4];//[0]用户码 [1]用户反码 [2]按键码 [3]按键反码 static void new_work_func(struct work_struct *work) { u8 data=0; u32 time_us; /*1. 判断引导码*/ time_us=GetTimeL(); if(time_us>12000 || time_us <7000)return; //标准9000 time_us=GetTimeH(); if(time_us>6000 || time_us <3000)return; //标准4500 /*2. 接收32位数据*/ int i,j; for(i=0;i<4;i++) { for(j=0;j<8;j++) { time_us=GetTimeL(); if(time_us>700 || time_us <400)return; //标准560 time_us=GetTimeH(); if(time_us<700 && time_us>400) // 0 :标准560 { data<<=1; } else if(time_us<1800 && time_us>1500) // 1 :标准1680 { data<<=1; data|=0x01; } else { return; } } buf[i]=data; } printk("用户码:%d,按键码:Ø1a8aca7-fa34-4a10-bd59-0edd11933a68n",buf[0],buf[2]); } /*中断服务函数*/ irqreturn_t nec_irq_handler_func(int irq, void *dev) { /*添加工作到工作队列*/ schedule_work(&work); return IRQ_HANDLED; } static int __init tiny4412_hello_module_init(void) { /*初始化工作函数*/ INIT_WORK(&work,new_work_func); /*1. 获取中断号*/ irq=gpio_to_irq(NEC_INFRARED_GPIO); /*2. 注册中断*/ request_irq(irq,nec_irq_handler_func,IRQF_TRIGGER_FALLING,"tinyy412_nec",NULL); printk("驱动测试: 驱动安装成功\n"); return 0; } static void __exit tiny4412_hello_module_cleanup(void) { free_irq(irq,NULL); printk("驱动测试: 驱动卸载成功\n"); } module_init(tiny4412_hello_module_init); /*驱动入口--安装驱动的时候执行*/ module_exit(tiny4412_hello_module_cleanup); /*驱动出口--卸载驱动的时候执行*/ MODULE_LICENSE("GPL"); /*设置模块的许可证--GPL*/
-
我在使用上层应用控制下层ESP32,需要用到平台下发命令,我是用Postman调测通过,但是使用Python时却报了{"error_code":"IOTDA.000001","error_msg":"Internal server error."}以下是我的代码import requests url = "XXXX" payload = { "service_id" : "SmokeDetectorControl", "command_name" : "ON_OFF", "paras" : { "value" : "1" } } headers = { 'x-auth-token': 'XXXXXX' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)与API explorer示例代码基本一致,希望哪位朋友能帮我看一下,不胜感激
-
一、前言当前的场景是,在高速公路上部署温度采集设备,在高速路地表安装温度检测传感器,检测当前路段的路面实际温度。一段高速路上有多个地点需要采集温度数据。 采集温度数据需要上传到云平台进行数据存储,并且通过可视化界面展示温度变化曲线,支持查询最近几天的温度信息。二、设计思路(1)云平台选型:使用华为云物联网云平台。(2)云数据存储: 使用OBS存储,存放设备上传的历史数据。(3)设备选项:NBIOT模块+温度采集模块,实现温度采集上报。(4)数据可视化:采用华为云IoT应用侧接口,获取传感器设备上传到云端的数据,在本地设计界面进行可视化显示温度数据。下面是温度数据可视化展示效果:本篇文章主要介绍设备上云的详细流程,介绍华为云物联网云端产品、设备创建流程,数据转存方式,应用侧开发接口等等。硬件选型:(1)STM32开发板: STM32F103C8T6(2)NBIOT模块--BC26BC26模块是一款高性能、低功耗、多频段LTE Cat NB1无线通信模块。(3)温度采集模块pt100是铂热电阻,它的阻值会随着温度的变化而改变。PT后的100即表示它在0℃时阻值为100欧姆,在100℃时它的阻值约为138.5欧姆。其工作原理:当PT100在0℃时,其电阻为100欧姆。它的电阻会随着温度的升高而上升,并且它的电阻会匀速增加。热电阻是一种常用于中低温的温度传感器。它的工作原理是基于电阻的热效应,即电阻的阻值随温度的变化而变化。铂金热敏电阻的热阻精度最高,具有抗振动、稳定性好、耐高压等特点。因此,它被制成各种标准温度计进行测量和校准。三、华为云IOT平台3.1 创建产品官网地址: cid:link_12(1)设备接入IOTDA在产品页面找到iot物联网,选择设备接入IOTDA设备接入服务(IoT Device Access)是华为云的物联网平台,提供海量设备连接上云、设备和云端双向消息通信、批量设备管理、远程控制和监控、OTA升级、设备联动规则等能力,并可将设备数据灵活流转到华为云其他服务,帮助物联网行业用户快速完成设备联网及行业应用集成,基础版每月一百万条消息免费。在页面上选择免费试用。点击后,会进入到设备接入控制台页面。(2)设备接入地址在基础版详情页面,点击右边的按需计费详情,可以查看物联网服务器接入的IP地址,端口号、接入方式。如果是设备接入,可以选择MQTT或者MQTTS协议,在单片机上只有MQTT协议验证比较方便,通过MQTT三元组即可完成设备连接,当前我这里的设备选择是MQTT协议连接华为云平台。 域名:a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com IP地址: 121.36.42.100 端口号: 1883(3)创建产品在左边选项卡里点击产品,进入产品页面,点击右上角创建产品。根据自己的产品信息,填写表单:(4)完成产品创建点击查看详情,可以进入到创建成功的产品页面。(4)定义产品模型产品创建之后,接着需要在平台上构建一款设备的抽象模型,使平台理解该款设备支持的功能。比如:温度采集设备肯定会向云平台上传采集的温度信息;在产品模型里就可以定义一个温度的属性字段。选择下面的自定义模型。添加服务ID。添加属性。新增属性字段。当前在多节点温度采集的设备里,主要是采集温度上传,这里就新增一个温度的属性。产品模型创建完成。3.2 创建设备(1)创建单个设备设备注册的方式有很多:【1】创建支持单个设备手动创建。【2】如果设备特别多可以选择批量注册。【3】通过API接口进行动态注册。当前为了演示流程,这里选择第一种方式,手动创建单个设备。在设备页面,选择所有设备选项,点击右边的注册设备按钮。(2)单设备注册填写信息点击注册设备之后,会弹出一个表单填写信息。其中产品就选择刚才创建的产品,设备标识码这一项一般是填设备的ID(设备的唯一标识符,方便绑定设备)。目前还没有对接硬件,我这里就填dev1,方便接下来的测试。 下面的设备ID这一项如果不填,会自动生成,可以不管;最后输入密匙(这个密匙的作用:通过对每个设备进行身份验证,可以安全地将每个设备连接到平台,并且安全地管理这些设备),填好之后点击确定。(3)设备创建完成创建好之后,保存生成的设备ID和密匙。得到的密匙和ID的格式文本如下: { "device_id": "6353a8163ec34a6d03c8dfe5_dev1", "secret": "12345678" }这个设备密匙和ID,后面生成MQTT登录参数时需要用到。(4)创建多个温度设备节点由于本项目是实现多节点温度上传到云平台,每个节点都是一个独立的温度采集设备,为了方便演示效果,还需要多创建几个设备。接下来的创建流程,和刚才第一个设备一样,这里就不再截图演示了。点击创建设备按钮,继续注册。目前一共创建了4个设备,其中main_dev设备是用来作为显示终端,在本地用显示屏显示其他温度采集节点采集的温度信息。 剩下3个设备dev1,dev2,dev3是表示3个独立的温度采集节点。这4个设备的密匙和ID信息如下: { "device_id": "6353a8163ec34a6d03c8dfe5_dev1", "secret": "12345678" } { "device_id": "6353a8163ec34a6d03c8dfe5_dev2", "secret": "12345678" } { "device_id": "6353a8163ec34a6d03c8dfe5_dev3", "secret": "12345678" } { "device_id": "6353a8163ec34a6d03c8dfe5_main_dev", "secret": "12345678" }3.3 模拟设备上云测试目前设备创建之后,这些设备都还没有激活。接下来会使用MQTT客户端来模拟真实设备上云,上传温度数据。这里的上云,包括数据交互都采用MQTT客户端来模拟实现,不涉及到实际的硬件,只要模拟能测试成功,并且能得到自己想要的结果,那硬件就没有问题了。(1)生成MQTT鉴权三元组设备要连接华为云平台的方式,在第一节创建产品的时候就已经介绍了,本次项目里的设备是采用MQTT协议接入云平台。 在完成设备模拟上云之前,需要先生成设备的MQTT协议鉴权三元组。华为云提供了一个在线工具,用来生成MQTT鉴权三元组: cid:link_8工具打开的界面如下效果:前面两行就是填设备创建后生成的设备ID和设备密匙,填好之后,生成下面3行信息,生成的3行就是MQTT协议登录需要用的参数。按照格式分别生成4个设备的鉴权信息:得到的三元组如下: ClientId 6353a8163ec34a6d03c8dfe5_dev1_0_0_2022102209 Username 6353a8163ec34a6d03c8dfe5_dev1 Password c58c45d514832b119b4302eafb3e74854849ca94079e9aed75efedf176e9c388 ClientId 6353a8163ec34a6d03c8dfe5_dev2_0_0_2022102209 Username 6353a8163ec34a6d03c8dfe5_dev2 Password c58c45d514832b119b4302eafb3e74854849ca94079e9aed75efedf176e9c388 ClientId 6353a8163ec34a6d03c8dfe5_dev3_0_0_2022102209 Username 6353a8163ec34a6d03c8dfe5_dev3 Password c58c45d514832b119b4302eafb3e74854849ca94079e9aed75efedf176e9c388 ClientId 6353a8163ec34a6d03c8dfe5_main_dev_0_0_2022102209 Username 6353a8163ec34a6d03c8dfe5_main_dev Password c58c45d514832b119b4302eafb3e74854849ca94079e9aed75efedf176e9c388(2)MQTT客户端模拟设备登录得到MQTT三元组之后,接下来用MQTT客户端模拟设备登录云平台。按照软件的输入框提示,输入对应的信息,点击登录。 其中的IP地址和端口号在第一小节的产品创建里就介绍过了。 后面输入的这3行就是上一步生成的MQTT鉴权三元组。登录成功。然后打开云平台的控制台,查看设备在线情况:可以看到dev1已经在线了, 刚才模拟的设备就是dev1。(3)主题订阅与发布目前设备已经成功登录,接下来要解决的问题就是数据传输问题了。MQTT协议里要理解的两个概念就是主题订阅,主题发布。 设备上传数据到平台,属于 主题发布。 设备想要知道其他设备的数据或者云平台下发的指令,需要进行主题订阅。帮助文档的地址: cid:link_6MQTT消息由固定报头(Fixed header)、可变报头(Variable header)和有效载荷(Payload)三部分组成。其中固定报头(Fixed header)和可变报头(Variable header)格式的填写请参考MQTT标准规范,有效载荷(Payload)的格式由应用定义,即设备和物联网平台之间自己定义。常见MQTT消息类型主要有CONNECT、SUBSCRIBE、PUBLISH。 CONNECT:指客户端请求和服务端连接。有效载荷(Payload)的主要参数,参考设备连接鉴权填写。 SUBSCRIBE:指客户端订阅请求。有效载荷(Payload)中的主要参数“Topic name”,参考Topic定义中订阅者为设备的Topic。 PUBLISH:平台发布消息。 可变报头(Variable header)中的主要参数“Topic name”,指设备上报到物联网平台时发布者为设备的Topic。详细请参考Topic定义。 有效载荷(Payload)中的主要参数为完整的数据上报和命令下发的消息内容,目前是一个JSON对象。 上行Topic是指设备向平台发送请求,或上报数据,或回复响应。 下行Topic是指平台向设备下发指令,或回复响应。 设备与平台建立连接后,需要订阅下行Topic,否则无法收到平台下发的指令或回复的响应。应用侧接口的调用,需要设备侧的配合,例如应用侧下发命令,设备侧需要先订阅“平台命令下发”的下行Topic,否则设备无法收到平台命令,应用下发命令的接口也会报超时。在产品页面,可以看到主题的格式,以及对于主题的用途:**【1】订阅主题对于设备而言,一般会订阅平台下发消息给设备 这个主题。设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。主题的格式如下: $oc/devices/{device_id}/sys/messages/down 以dev1设备1为例,最终的格式: $oc/devices/6353a8163ec34a6d03c8dfe5_dev1/sys/messages/down【2】发布主题对于设备,发布主题,也就显示向云平台上传数据。发布的主题格式如下: $oc/devices/{device_id}/sys/properties/report 以dev1设备1为例最终的格式: $oc/devices/6353a8163ec34a6d03c8dfe5_dev1/sys/properties/report发布主题时,需要上传数据,这个数据格式是JSON格式。上传的JSON数据格式如下: { "services": [ { "service_id": <填服务ID>, "properties": { "<填属性名称1>": <填属性值>, "<填属性名称2>": <填属性值>, .......... } } ] }根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。根据这个格式,组合温度节点一次上传的数据: {"services": [{"service_id": "temp","properties":{"temp":24.6}}]}(4)MQTT客户端模拟设备上报数据打开MQTT客户端填入订阅主题,发布主题,和需要发布的数据,然后分别点击订阅主题按钮,发布主题按钮。 右边提示成功之后,就可以打开云平台看上传的效果了。 (这里是以dev1,设备1为例)打开云平台看到dev1已经在线。点击dev1,进去查看设备上传的数据。 可以看到,刚才上传的数据已经收到了。到此,设备上传数据到云平台已经完成。四、设备数据转存如果设备上传的数据需要进行保存,后续进行分析做其他用途,可以利用数据转发服务,让平台将设备上报数据推送给自己的应用服务器,由应用服务器进行保存;也可以选择让平台将设备上报数据转发给OBS对象存储服务,由OBS进行存储,进行永久保存,非常方便。如果存储在OBS里,自己设计应用侧界面时,也可以直接拉取OBS里的数据下来进行显示,处理,分析。4.1 创建OSB存储桶地址: cid:link_11对象存储服务(Object Storage Service,OBS)是一个基于对象的存储服务,提供了海量、安全、高可靠、低成本的数据存储能力。(1)选择管理控制台(2)创建桶填充桶信息: 我这里选择的是 华北-北京一我这里因为要长期使用,这里选择1年的购买权。(3)创建成功4.2 配置数据转发规则(1)创建规则选择左侧导航栏的规则>数据转发,单击右上角的创建规则。填充规则转发的信息:参数名参数说明规则名称自定义,如: led_obs。规则描述自定义,如数据转发至OBS服务。数据来源选择“设备属性”。触发事件自动匹配“设备属性上报”。资源空间和上一步创建的产品所在的资源空间保持一致。(2)设置转发目标单击添加,设置转发目标。这里的区域选择--华北-北京一。 因为前面的OBS桶创建的时候,设置区域设置的是北京一,然后点击授权。授权之后,选择刚才创建的OBS桶,设置存储数据的目录和文件名字。存储的数据可以直接转存JSON数据到OBS存储桶,也可以存放成CSV文件到存储桶。如果选择存储JSON,就是直接将设备上传的数据存放到OBS存储里,如果选择存储CSV文件,可以自己选择需要存储的字段。 下面我演示一下存储成CSV文件时如何进行设置。 (选择JSON文件不需要进行任何设置,直接将设备上传的数据JSON存储进去了)。下面设置转发的字段: (如果提示没有授权,点击授权即可)这个转发字段就是表示需要存放的数据是那些,对于路灯而言,肯定是需要存储上报的温度、湿度、电量、光照强度的属性的。 这些属性在创建产品的时候设置,设备上报的也是这些属性。这是设备上传一次云平台的完整JSON数据格式: { "resource": "device.property", "event": "report", "event_time": "20221018T131627Z", "request_id": "5ee95a0c-262d-43c3-8d31-af453f9952ef", "notify_data": { "header": { "app_id": "7211833377cf435c8c0580de390eedbe", "device_id": "634e3e423ec34a6d03c84bfb_1126626497", "node_id": "1126626497", "product_id": "634e3e423ec34a6d03c84bfb", "gateway_id": "634e3e423ec34a6d03c84bfb_1126626497" }, "body": { "services": [ { "service_id": "temp", "properties": { "temp": 28, }, "event_time": "20221018T131627Z" } ] } } }当前设备上传到云端服务器有一个温度属性字段temp,如果想转发设备上传的这个温度字段,就可以这样写: notify_data.body.services[0].properties.temp后面的存储目标字段,为了好区分,直接填temp即可。如果设备还上传了其他属性字段也想转发,按照上面格式设置即可。如果存储数据时,想知道这个数据是那个产品,那个设备上传的,也可以将设备ID和产品ID转发存储起来:格式如下。 notify_data.header.app_id notify_data.header.device_id下面是我设置好,转发存储的字段:确定后,点击设置完成。(3)测试规则设置好之后,在设置转发目标的页面点击测试。输入数据模板,然后点击连通性测试。如果测试结果显示成功,说明整体流程没有问题了。(4)启用规则设置完成后,点击启用规则。(6)数据上报测试为了验证转发规则是否生效,接下来使用MQTT客户端多上报几次数据到云平台。上传之后,打开OBS存储桶的控制台页面。打开转发规则存储的OBS桶。找到存储数据的文件,点击下载。下载下来,打开可以看到存储的数据:到此,数据转发,存储已经成功了。五、应用侧-可视化大屏开发对于数据的可视化显示,华为云提供了(Data Lake Visualization)一站式数据可视化平台,数据可视化服务(DLV)可以从OBS文件读取数据呈现为可视化报表,实现数据的可视化显示。下面是华为云的DLV大屏从OBS读取数据显示的流程:当前我这里的需求是需要在本地自己设计界面显示数据,没有采用华为云的DLV大屏,如果自己本地软件需要显示设备上传的数据,就需要使用华为云物联网的平台的应用侧API接口,读取设备上传的数据进行,本地进行显示;如果需要历史数据,可以读取OBS存储桶里存储的数据进行显示。5.1 应用侧接口帮助文档地址: cid:link_45.2 查询设备影子数据接口应用侧接口可以查询发送指令给设备查询属性,也可以读取设备的影子数据。【1】查询设备查询: 这个是实时查询,相当于应用侧接口发送指令给在线设备,设备收到指令,将当前最新的数据再上传。这个需要保证设备在线,离线是无法调用的。【2】影子数据:影子数据相当于保存设备上传的最新一次数据。 读取读取影子设备数据,是不需要设备在线。(1)接口URI地址请求方法GETURI/v5/iot/{project_id}/devices/{device_id}/shadow传输协议HTTPS(2)请求参数说明名称必选/可选类型位置说明X-Auth-Token必选StringHeader参数说明:用户Token。通过调用IAM服务 获取IAM用户Token接口获取,接口返回的响应消息头中“X-Subject-Token”就是需要获取的用户Token。Instance-Id可选StringHeader参数说明:实例ID。物理多租下各实例的唯一标识,一般华为云租户无需携带该参数,仅在物理多租场景下从管理面访问API时需要携带该参数。project_id必选StringPath参数说明:项目ID。获取方法请参见 获取项目ID。device_id必选StringPath参数说明:设备ID,用于唯一标识一个设备。在注册设备时直接指定,或者由物联网平台分配获得。由物联网平台分配时,生成规则为"product_id" + " " + "node_id"拼接而成。取值范围:长度不超过128,只允许字母、数字、下划线( )、连接符(-)的组合。(3)响应参数名称类型说明device_idString设备ID,用于唯一标识一个设备。在注册设备时直接指定,或者由物联网平台分配获得。由物联网平台分配时,生成规则为"product_id" + "_" + "node_id"拼接而成。shadowList[DeviceShadowData]()设备影子数据结构体。 名称类型说明service_idString设备的服务ID,在设备关联的产品模型中定义。desiredDeviceShadowProperties Object用户最近一次对设备下发的预期数据,Json格式,里面是一个个键值对,每个键都是产品模型中属性的参数名(property_name)。reportedDeviceShadowProperties Object设备最近一次上报的属性数据,Json格式,里面是一个个键值对,每个键都是产品模型中属性的参数名(property_name)。versionLong设备影子的版本,携带该参数时平台会校验值必须等于当前影子版本,初始从0开始。 名称类型说明propertiesObject设备影子的属性数据,Json格式,里面是一个个键值对,每个键都是产品模型中属性的参数名(property_name),目前如样例所示只支持一层结构。注意:JSON结构的key当前不支持特殊字符:点(.)、dollar符号($)、空char(十六进制的ASCII码为00),key为以上特殊字符无法正常刷新设备影子event_timeString事件操作时间,格式:yyyyMMdd'T'HHmmss'Z',如20151212T121212Z。(4)请求示例 GET https://{Endpoint}/v5/iot/{project_id}/devices/{device_id}/shadow Content-Type: application/json X-Auth-Token: ******** Instance-Id: ********(5)响应示例Status Code: 200 OK Content-Type: application/json { "device_id" : "40fe3542-f4cc-4b6a-98c3-61a49ba1acd4", "shadow" : [ { "service_id" : "WaterMeter", "desired" : { "properties" : { "temperature" : "60" }, "event_time" : "20151212T121212Z" }, "reported" : { "properties" : { "temperature" : "60" }, "event_time" : "20151212T121212Z" }, "version" : 1 } ] }(6)错误码HTTP状态码错误码错误码英文描述错误码中文描述处理建议403IOTDA.000021Operation not allowed. User not found by IAM token or the authorized user has not subscribed IOTDA service.没有找到IAM Token所对应的用户信息或该用户没有订阅设备接入服务(IOTDA)请排查IAM Token所在用户是否订阅了设备接入服务(IOTDA)。404IOTDA.014000The device does not exist.设备不存在请排查请求参数是否有误并确认是否有在平台注册该设备。500IOTDA.000020Decrypt IAM token failed.5.3 接口调试在线调试地址:https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=IoTDA&api=ShowDeviceShadow下面是调试影子 数据查询接口,查询设备影子数据: 右边返回的是设备上传的最新数据。5.5 接口总结 请求地址: https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/{project_id}/devices/{device_id}/shadow 请求方式: GET 请求头: { "X-Auth-Token": "这个需要自己获取", "Content-Type": "application/json" }5.5 如何获取X-Subject-Token使用API访问华为云的所有服务接口,都需要填X-Subject-Token参数,下面介绍步骤:(1)创建一个新的IAM帐户鼠标悬停在右上角的用户名称上,弹出下拉框,选择统一身份认证。(2)选择创建用户(3)使用调试接口测试获取oken调试接口地址: https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=IAM&api=KeystoneCreateUserTokenByPassword右边响应头里的X-Subject-Token就是获取的token。(4)上面的这些账户名称从哪里获取?(5)请求地址和数据格式获取X-Subject-Token请求的地址: cid:link_10请求头数据: { "User-Agent": "API Explorer", "X-Auth-Token": "******", "Content-Type": "application/json;charset=UTF-8" }请求体数据: { "auth": { "identity": { "methods": [ "password" ], "password": { "user": { "domain": { "name": "xxxxx" //这里填当前主账户名称 }, "name": "xxxx", //这个新建的子账户名称 "password": "xxxxx" //这个是新建的子账户密码 } } }, "scope": { "project": { "name": "cn-north-4" } } } }(6)代码实现 /* 功能: 获取token */ void Widget::GetToken() { //表示获取token function_select=3; QString requestUrl; QNetworkRequest request; //设置请求地址 QUrl url; //获取token请求地址 requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens") .arg(SERVER_ID); //自己创建的TCP服务器,测试用 //requestUrl="http://10.0.0.6:8080"; //设置数据提交格式 request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8")); //构造请求 url.setUrl(requestUrl); request.setUrl(url); QString text =QString("{"auth":{"identity":{"methods":["password"],"password":" "{"user":{"domain": {" ""name":"%1"},"name": "%2","password": "%3"}}}," ""scope":{"project":{"name":"%4"}}}}") .arg(MAIN_USER) .arg(IAM_USER) .arg(IAM_PASSWORD) .arg(SERVER_ID); //发送请求 manager->post(request, text.toUtf8()); }5.6 查询设备影子数据代码(1)这是请求代码 //查询设备属性 void Widget::Get_device_properties() { //表示获取token function_select=0; QString requestUrl; QNetworkRequest request; //设置请求地址 QUrl url; //获取token请求地址 requestUrl = QString("https://iotda.%1.myhuaweicloud.com/v5/iot/%2/devices/%3/shadow") .arg(SERVER_ID) .arg(PROJECT_ID) .arg(device_id); //设置数据提交格式 request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json")); //设置token request.setRawHeader("X-Auth-Token",Token); //构造请求 url.setUrl(requestUrl); request.setUrl(url); //发送请求 manager->get(request); } //更新设备属性 void Widget::on_pushButton_update_device_clicked() { Get_device_properties(); }(2)这是请求一次返回的JSON数据 { "device_id": "6353a8163ec34a6d03c8dfe5_dev1", "shadow": [ { "service_id": "temp", "desired": { "properties": null, "event_time": null }, "reported": { "properties": { "temp": 45.6 }, "event_time": "20221024T013607Z" }, "version": 49 } ] }5.7 界面设计最终效果六、硬件部分6.1 MQTT版本注意:华为云、OneNet、腾讯IOT 等平台规定接入的MQTT协议版本必须是3.1.1 。在BC26里,需要执行这行代码配置MQTT协议版本为3.1.1 AT+QMTCFG="version",0,46.2 BC26上云配置代码 #include "BC26.h" //BC26复位 //引脚是PC0 //高电平有效 void BC26_Reset(void) { //开时钟 RCC->APB2ENR|=1<<4; //配置GPIO口 GPIOC->CRL&=0xFFFFFFF0; GPIOC->CRL|=0x00000003; //开始复位 GPIOC->ODR|=1<<0; DelayMs(2000); GPIOC->ODR&=~(1<<0); } /* 函数功能:向BC26模块发送指令 函数参数: char *cmd 发送的命令 char *check_data 检测返回的数据 返回值: 0表示成功 1表示失败 */ u8 BC26_SendCmd(char *cmd,char *check_data) { u16 i,j; for(i=0;i<5;i++) //测试的总次数 { USART2_RX_FLAG=0; USART2_RX_CNT=0; memset(USART2_RX_BUFFER,0,sizeof(USART2_RX_BUFFER)); USARTx_StringSend(USART2,cmd); //发送指令 for(j=0;j<500;j++) //等待的时间(ms单位) { if(USART2_RX_FLAG) { USART2_RX_BUFFER[USART2_RX_CNT]='\0'; if(strstr((char*)USART2_RX_BUFFER,check_data)) { return 0; } else break; } delay_ms(30); //一次的时间 } } return 1; } //初始化BC26模块 int BC26_Init(void) { if(BC26_SendCmd("AT\r\n","OK")) { USART1_Printf("BC26模块不存在.\r\n"); return 1; } else { USART1_Printf("BC26模块正常!\r\n"); } if(BC26_SendCmd("AT+CIMI\r\n","OK")) { USART1_Printf("模块未插卡.\r\n"); return 3; } else { USART1_Printf("卡已经插好.\r\n"); } if(BC26_SendCmd("AT+CGATT=1\r\n","OK")) { USART1_Printf("配置:网络激活失败.\r\n"); return 3; } else { USART1_Printf("配置:网络激活成功.\r\n"); } if(BC26_SendCmd("AT+CGATT?\r\n","OK")) { USART1_Printf("状态:网络激活失败.\r\n"); return 4; } else { USART1_Printf("状态:网络激活成功.\r\n"); } if(BC26_SendCmd("AT+CSQ\r\n","OK")) { USART1_Printf("查询信号质量失败.\r\n"); return 5; } else { USART1_Printf("信号质量:%s\r\n",USART2_RX_BUFFER); } // if(BC26_SendCmd("AT+QGNSSC=1\r\n","OK")) // { // USART1_Printf("激活GPS定位失败.\r\n"); // return 6; // } // else // { // USART1_Printf("激活GPS定位成功.\r\n"); // } // // if(BC26_SendCmd("AT+QGNSSAGPS=1\r\n","OK")) // { // USART1_Printf("开启AGPS定位失败.\r\n"); // return 6; // } // else // { // USART1_Printf("开启AGPS定位成功.\r\n"); // } // // if(BC26_SendCmd("AT+CGPADDR=1\r\n","OK")) // { // USART1_Printf("激活GPRS场景失败.\r\n"); // return 7; // } // else // { // USART1_Printf("激活GPRS场景成功.\r\n"); // } // // DelayMs(1000); // DelayMs(1000); if(BC26_SendCmd("AT+CEREG?\r\n","+CEREG: 0,1")) { USART1_Printf("网络注册状态:失败.\r\n"); return 9; } else { USART1_Printf("网络注册状态:成功.\r\n"); } return 0; } //发送使用的缓冲区 char BC26_SEND_BUFF[500]; //MQTT协议登录服务器 int BC26_MQTT_Connect(void) { //1. 先关闭之前的连接 USART1_Printf("正在关闭之前的连接...\r\n"); BC26_SendCmd("AT+QMTCLOSE=0\r\n","OK"); DelayMs(4000); //关闭服务 BC26_SendCmd("AT+QMTCONN?\r\n","OK"); DelayMs(4000); //2. 连接MQTT服务器 USART1_Printf("正在连接MQTT服务器..\r\n"); sprintf(BC26_SEND_BUFF,"AT+QMTOPEN=0,"%s",%s\r\n",MQTT_SERVER_ADDR,MQTT_SERVER_PORT); if(BC26_SendCmd(BC26_SEND_BUFF,"OK")) { USART1_Printf("MQTT服务器连接失败:%s\r\n",BC26_SEND_BUFF); return 1; } else { USART1_Printf("MQTT服务器连接成功.\r\n"); } DelayMs(3000); //3. 登录MQTT服务器 USART1_Printf("正在登录MQTT服务器...\r\n"); sprintf(BC26_SEND_BUFF,"AT+QMTCONN=0,"%s","%s","%s"\r\n",MQTT_CLIENT_ID,MQTT_USERNAME,MQTT_PASSWORD); if(BC26_SendCmd(BC26_SEND_BUFF,"OK")) { USART1_Printf("MQTT服务器登录失败:%s\r\n",BC26_SEND_BUFF); return 2; } else { USART1_Printf("MQTT服务器登录成功.\r\n"); } DelayMs(3000); //4. 订阅主题 USART1_Printf("正在订阅主题...\r\n"); sprintf(BC26_SEND_BUFF,"AT+QMTSUB=0,1,"%s",2\r\n",MQTT_TOPIC_SUB_GET); if(BC26_SendCmd(BC26_SEND_BUFF,"OK")) { USART1_Printf("MQTT主题订阅失败:%s\r\n",BC26_SEND_BUFF); } else { USART1_Printf("MQTT主题订阅成功.\r\n"); } return 0; } //MQTT发布主题 int MQTT_PublishTheme(char *text) { char send_buf[3]; sprintf(BC26_SEND_BUFF,"AT+QMTPUB=0,0,0,0,"%s"\r\n",MQTT_TOPIC_SUB_SET); if(BC26_SendCmd(BC26_SEND_BUFF,">")) { USART1_Printf("发布主题等待输入失败:%s\r\n",BC26_SEND_BUFF); return 1; } USARTx_StringSend(USART2,text); //发送主题内容 //发送结束符 send_buf[0] = 0x1a; send_buf[1] = '\0'; if(BC26_SendCmd(send_buf,"OK")) { USART1_Printf("发布主题内容失败:%s\r\n",BC26_SEND_BUFF); return 2; //发送结束符号 } else { USART1_Printf("发布主题内容成功.\r\n"); } USART1_Printf("发布主题内容:%s\r\n",text); return 0; } //MQTT响应应用层的属性请求 int MQTT_SendAttribute(char *text,char *request_id) { char send_buf[3]; sprintf(BC26_SEND_BUFF,"AT+QMTPUB=0,0,0,0,"%s%s"\r\n",MQTT_TOPIC_SUB_SET_RUN,request_id); if(BC26_SendCmd(BC26_SEND_BUFF,">")) { USART1_Printf("(响应)发布主题等待输入失败:%s\r\n",BC26_SEND_BUFF); return 1; } USARTx_StringSend(USART2,text); //发送主题内容 //发送结束符 send_buf[0] = 0x1a; send_buf[1] = '\0'; if(BC26_SendCmd(send_buf,"OK")) { USART1_Printf("(响应)发布主题内容失败:%s\r\n",BC26_SEND_BUFF); return 2; //发送结束符号 } else { USART1_Printf("(响应)发布主题内容成功.\r\n"); } USART1_Printf("(响应)发布主题内容:%s\r\n",text); return 0; } /* 函数功能: 获取一次GPS经纬度数据 函数参数: double *Longitude :经度 double *latitude :纬度 返回值: 0表示定位成功,1表示数据接收失败,2表示定位失败 */ u8 BC26_GetGPS_Data(double *Longitude,double *latitude) { /*1. 发送获取GPS数据的指令*/ if(BC26_SendCmd("AT+QGNSSRD="NMEA/RMC"\r\n", "OK\r\n"))return 1; /*2. 对GPS数据进行解码*/ if(GPS_GNRMC_Decoding((char *)USART2_RX_BUFFER,Longitude,latitude))return 2; //解码成功 return 0; } /* 函数功能: 开启GPS功能 返 回 值:0表示成功 1表示失败 */ u8 BC26_StartGPS(void) { //先判断GPS功能是否启动 if(BC26_SendCmd("AT+QGNSSC?\r\n","+QGNSSC: 1")) { //没有启动就启动GPS功能 if(BC26_SendCmd("AT+QGNSSC=1\r\n","OK\r\n")) { USART1_Printf("开启GPS功能失败.\r\n"); return 1; //GPS功能启动失败 } else { USART1_Printf("开启GPS功能成功.\r\n"); } } return 0; }6.3 MQTT 3.1协议介绍地址: cid:link_5 规范共分七章: 第一章 - 介绍 第二章 - MQTT控制包格式 第三章 - MQTT控制包 第四章 - 操作行为 第五章 - 安全 第六章 - 使用WebSocket进行网络传输 第七章 - 一致性目标
-
1. 前言OLED显示屏在是智能手环,智能手表上用的非常的多,功耗低,不刺眼,优点特别多。本篇文章就介绍,在Linux系统里如何使用OLED显示屏,要使用OLED显示屏,大致分为两步: (1) 针对OLED显示屏编写一个驱动 (2) 编写应用层程序进行测试。采用的OLED显示屏是0.96寸SPI接口显示屏,分辨率是128*64,比较便宜,淘宝上非常多。测试开发板采用友善之臂Tiny4412,三星的EXYNOS-4412芯片,4核1.5GHZ,板载8G-EMMC,2G-DDR。2. 硬件接线效果3. 驱动代码Linux内核提供了标准SPI子系统框架,和前面介绍的IIC子系统框架使用类似,代码分为设备端和驱动端,Linux内核提供子系统的目的就是为了统一驱动编写标准,提高驱动代码的移植性。本篇文章代码没有采用SPI子系统框架,采用单片机惯用的模拟SPI时序,对入门而言,代码更容易理解。3.1 oled.c 驱动示例代码#include <linux/kernel.h>#include <linux/module.h>#include <linux/miscdevice.h> #include <linux/fs.h>#include <linux/uaccess.h>#include <linux/fb.h>#include <linux/io.h>#include <linux/mm.h>#include <linux/slab.h>#include <linux/gpio.h>#include <linux/delay.h>#include <mach/gpio.h>#include <plat/gpio-cfg.h>/*定义OLED需要使用的寄存器*/static volatile unsigned int *GPB_CON=NULL;static volatile unsigned int *GPB_DAT=NULL;//OLED屏幕底层接口#define OLED_SCK(x) if(x){*GPB_DAT|=1<<0;}else{*GPB_DAT&=~(1<<0);}#define OLED_MOSI(x) if(x){*GPB_DAT|=1<<3;}else{*GPB_DAT&=~(1<<3);}#define OLED_RES(x) if(x){*GPB_DAT|=1<<4;}else{*GPB_DAT&=~(1<<4);}#define OLED_DC(x) if(x){*GPB_DAT|=1<<5;}else{*GPB_DAT&=~(1<<5);}#define OLED_CS(x) if(x){*GPB_DAT|=1<<1;}else{*GPB_DAT&=~(1<<1);}//命令与数据区分#define OLED_CMD 0#define OLED_DAT 1//函数声明区域static void OLED_WriteOneByte(u8 data,u8 cmd);static u8 OLED_GPIO_Init(void);static void OLED_Init(void);static void OLED_Clear(u8 data);static void OLED_DrawPoint(u8 x,u8 y,u8 c);static void OLED_RefreshGRAM(void);/*函数功能: OLED对应的GPIO口初始化硬件连接:OLED模块---Tiny4412开发板GND--------GNDVCC--------VCC(5V)D0---------SCL--------------GPB_0D1---------MOSI-------------GPB_3RES--------复位-------------GPB_4DC---------数据/命令--------GPB_5CS---------CS片选-----------GPB_1*/static u8 OLED_GPIO_Init(void){ /*1. 将物理地址转换为虚拟地址*/ GPB_CON=ioremap(0x11400040,4); GPB_DAT=ioremap(0x11400044,4); if(GPB_CON==NULL||GPB_DAT==NULL) { printk("物理地址转换为虚拟地址出现问题!\n"); return -1; } /*2. 配置GPIO口模式*/ *GPB_CON&=0xFF000F00; *GPB_CON|=0x00111011; /*3. 上拉GPIO口*/ OLED_CS(1); OLED_DC(1); OLED_MOSI(1); OLED_RES(1); OLED_SCK(1);}/*函数功能: OLED屏幕初始化*/static void OLED_Init(void){ /*1. 初始化配置GPIO口*/ OLED_GPIO_Init(); /*2. 执行OLED屏幕的初始化配置*/ OLED_RES(1); udelay(2000); OLED_RES(0); udelay(2000); OLED_RES(1); udelay(2000); OLED_WriteOneByte(0xAE,OLED_CMD); //0xAE表示关显示,0xAF表示开显示 OLED_WriteOneByte(0x00,OLED_CMD); OLED_WriteOneByte(0x10,OLED_CMD); OLED_WriteOneByte(0x40,OLED_CMD); OLED_WriteOneByte(0xB0,OLED_CMD); OLED_WriteOneByte(0x81,OLED_CMD); OLED_WriteOneByte(0xCF,OLED_CMD); OLED_WriteOneByte(0xA1,OLED_CMD); OLED_WriteOneByte(0xA6,OLED_CMD); OLED_WriteOneByte(0xA8,OLED_CMD); OLED_WriteOneByte(0x3F,OLED_CMD); OLED_WriteOneByte(0xC8,OLED_CMD); OLED_WriteOneByte(0xD3,OLED_CMD); OLED_WriteOneByte(0x00,OLED_CMD); OLED_WriteOneByte(0xD5,OLED_CMD); OLED_WriteOneByte(0x80,OLED_CMD); OLED_WriteOneByte(0xD9,OLED_CMD); OLED_WriteOneByte(0xF1,OLED_CMD); OLED_WriteOneByte(0xDA,OLED_CMD); OLED_WriteOneByte(0x12,OLED_CMD); OLED_WriteOneByte(0xDB,OLED_CMD); OLED_WriteOneByte(0x30,OLED_CMD); OLED_WriteOneByte(0x8D,OLED_CMD); OLED_WriteOneByte(0x14,OLED_CMD); OLED_WriteOneByte(0xAF,OLED_CMD); //正常模式}/*函数功能: 写一个字节函数参数: cmd=0表示命令,cmd=1表示数据*/static void OLED_WriteOneByte(u8 data,u8 cmd){ u8 i; /*1. 区分发送数据是命令还是屏幕数据*/ if(cmd){OLED_DC(1);} else {OLED_DC(0);} udelay(2); /*2. 发送实际的数据*/ OLED_CS(0); //选中OLED for(i=0;i<8;i++) { udelay(2); OLED_SCK(0); //告诉从机,主机将要发送数据 if(data&0x80){OLED_MOSI(1);} //发送数据 else {OLED_MOSI(0);} udelay(2); OLED_SCK(1); //告诉从机,主机数据发送完毕 data<<=1; //继续发送下一位数据 } OLED_CS(1); //取消选中OLED OLED_SCK(1); //上拉时钟线,恢复空闲电平}/*函数功能: 清屏 (开全部灯、关全部灯)*/static void OLED_Clear(u8 data){ u8 i,j; for(i=0;i<8;i++) { OLED_WriteOneByte(0xB0+i,OLED_CMD); //设置页地址 OLED_WriteOneByte(0x10,OLED_CMD); //设置列高起始地址(半字节) OLED_WriteOneByte(0x00,OLED_CMD); //设置列低起始地址(半字节) for(j=0;j<128;j++) { OLED_WriteOneByte(data,OLED_DAT); //写数据 } }}/*定义显存数组: 8行,每行128列,与OLED屏幕对应*/static u8 OLED_GRAM[8][128]; /*函数功能: 画点函数 x: 横向坐标0~128 y: 纵坐标0~64 c: 1表示亮、0表示灭*/static void OLED_DrawPoint(u8 x,u8 y,u8 c){ u8 page; page=y/8; //得到当前点的页数0/8=0 1/8=0 y=y%8; //得到一列中点的位置。(0~7) //0%8=0 1%8=1 .....7%8=7 8%8=0 9%8=1 ...... if(c) OLED_GRAM[page][x]|=1<<y; else OLED_GRAM[page][x]&=~(1<<y);}/*函数功能: 刷新数据到OLED显示屏*/static void OLED_RefreshGRAM(void){ u8 i,j; for(i=0;i<8;i++) { OLED_WriteOneByte(0xB0+i,OLED_CMD); //设置页地址 OLED_WriteOneByte(0x10,OLED_CMD); //设置列高起始地址(半字节) OLED_WriteOneByte(0x00,OLED_CMD); //设置列低起始地址(半字节) for(j=0;j<128;j++) { OLED_WriteOneByte(OLED_GRAM[i][j],OLED_DAT); //写数据 } }}static u8 *mmap_buffer=NULL; /*指针-存放申请空间的首地址*/static int lcd_open(struct fb_info *info, int user){ mmap_buffer=kmalloc(4096,GFP_ATOMIC); if(mmap_buffer==NULL) { printk("空间申请失败!\n"); return 0; } printk("lcd_open调用成功\n"); return 0;}static int lcd_mmap(struct fb_info *info, struct vm_area_struct *vma){ vma->vm_flags |= VM_IO;//表示对设备 IO 空间的映射 vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出 if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里 vma->vm_start,//虚拟空间的起始地址 virt_to_phys(mmap_buffer)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移 12 位 vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍 vma->vm_page_prot))//保护属性, { return -EAGAIN; } printk("(drv)映射的长度:ße256e8d-437c-4a1b-a40c-ec65e12ac3ccn",vma->vm_end - vma->vm_start); printk("物理地址:0x%X\n",virt_to_phys(mmap_buffer)); /* 开发板的DDR容量: 1G 0x40000000 ~ 0x80000000 0x10000000=256M */ return 0;}#define _OLED_RefreshGRAM 0x12345 /*将应用层的数据刷新到OLED屏幕上*/#define _OLED_ClearGRAM 0x45678 /*将应用层的数据刷新到OLED屏幕上*/static int lcd_ioctl(struct fb_info *info, unsigned int cmd,unsigned long arg){ switch(cmd) { case _OLED_RefreshGRAM: memcpy(OLED_GRAM,mmap_buffer,1024); //拷贝数据 OLED_RefreshGRAM(); /*刷新*/ break; case _OLED_ClearGRAM: /*清屏*/ OLED_Clear(0); break; } return 0;}static int lcd_release(struct fb_info *info, int user){ if(mmap_buffer!=NULL) { kfree(mmap_buffer); } printk("lcd_release调用成功\n"); return 0;}/*帧缓冲设备专用的文件操作接口*/static struct fb_ops fbops={ .fb_open=lcd_open, .fb_release=lcd_release, .fb_mmap=lcd_mmap, .fb_ioctl=lcd_ioctl};/*帧缓冲的设备结构体*/static struct fb_info lcd_info={ .var= /*可变形参*/ { .xres=128, .yres=64, .bits_per_pixel=1 }, .fix= { .smem_len=4096, .line_length=128 }, .fbops=&fbops};static int __init tiny4412_oled_init(void){ /*1. 初始化OLED屏幕*/ OLED_Init(); OLED_Clear(0);//清屏为黑色 /*2. 帧缓冲驱动注册*/ if(register_framebuffer(&lcd_info)!=0) { printk("提示: lcd驱动安装失败!\n"); return -1; } else { printk("提示: lcd驱动安装成功!\n"); } return 0;}static void __exit tiny4412_oled_exit(void){ /*1. 帧缓冲驱动注销*/ if(unregister_framebuffer(&lcd_info)!=0) { printk("提示: lcd驱动卸载失败!\n"); return -1; } else { printk("提示: lcd驱动卸载成功!\n"); } /*2. 解除虚拟地址映射关系*/ iounmap(GPB_CON); iounmap(GPB_DAT);}module_init(tiny4412_oled_init); /*指定驱动的入口函数*/module_exit(tiny4412_oled_exit); /*指定驱动的出口函数*/MODULE_LICENSE("GPL"); /*指定驱动许可证*/复制代码3.2 app.c 应用层代码#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <linux/fb.h>#include <sys/mman.h>#include <string.h>unsigned char *lcd_mem=NULL; /*LCD的内存地址*/struct fb_fix_screeninfo finfo; /*固定形参*/struct fb_var_screeninfo vinfo; /*可变形参*/ unsigned char font[]={/*-- 文字: 国 --*//*-- 宋体42; 此字体下对应的点阵为:宽x高=56x56 宽/8*高*/0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0xC0,0x00,0x00,0x00,0x07,0x00,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xC0,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xE0,0x01,0xF0,0x00,0x00,0x00,0x07,0xC0,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x01,0x87,0x80,0x01,0xF0,0x00,0x00,0x03,0xC7,0x80,0x01,0xF7,0xFF,0xFF,0xFF,0xE7,0x80,0x01,0xF3,0xFF,0xFF,0xFF,0xF7,0x80,0x01,0xF1,0xC0,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x06,0x07,0x80,0x01,0xF0,0x00,0x7C,0x0F,0x07,0x80,0x01,0xF1,0xFF,0xFF,0xFF,0x87,0x80,0x01,0xF1,0xFF,0xFF,0xFF,0xC7,0x80,0x01,0xF0,0xF0,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7C,0x00,0x07,0x80,0x01,0xF0,0x00,0x7F,0xC0,0x07,0x80,0x01,0xF0,0x00,0x7D,0xF0,0x07,0x80,0x01,0xF0,0x00,0x7C,0xFC,0x07,0x80,0x01,0xF0,0x00,0x7C,0x7E,0x07,0x80,0x01,0xF0,0x00,0x7C,0x3F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x3F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x1F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x0F,0x07,0x80,0x01,0xF0,0x00,0x7C,0x0E,0x07,0x80,0x01,0xF0,0x00,0x7C,0x07,0x87,0x80,0x01,0xF0,0x00,0x7C,0x03,0xC7,0x80,0x01,0xF0,0x00,0x7C,0x07,0xE7,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xF7,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x80,0x01,0xF7,0x80,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x80,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x80,0x01,0xF0,0x00,0x00,0x00,0x07,0x00,0x01,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,};/*定义显存数组: 8行,每行128列,与OLED屏幕对应*/static unsigned char OLED_GRAM[8][128]; /*函数功能: 画点函数 x: 横向坐标0~128 y: 纵坐标0~64 c: 1表示亮、0表示灭*/static void OLED_DrawPoint(unsigned char x,unsigned char y,unsigned char c){ unsigned char page; page=y/8; //得到当前点的页数0/8=0 1/8=0 y=y%8; //得到一列中点的位置。(0~7) //0%8=0 1%8=1 .....7%8=7 8%8=0 9%8=1 ...... if(c) OLED_GRAM[page][x]|=1<<y; else OLED_GRAM[page][x]&=~(1<<y); memcpy(lcd_mem,OLED_GRAM,1024);}/*保证字体宽和高是一样的,而且必须是8的倍数。*/void ShowFont(int x,int y,int size,char *font){ int i,j,x0=x; unsigned char data; for(i=0;i<size/8*size;i++) { data=font[i]; for(j=0;j<8;j++) { //高位取模 if(data&0x80) { //字体颜色 OLED_DrawPoint(x0,y,1); } else { //背景颜色 OLED_DrawPoint(x0,y,0); } x0++; data<<=1; } if(x0-x==size) { x0=x; y++;//换行 } }}#define OLED_RefreshGRAM 0x12345 /*将应用层的数据刷新到OLED屏幕上*/#define OLED_ClearGRAM 0x45678 /*将应用层的数据刷新到OLED屏幕上*/int main(int argc,char **argv){ /*1.打开设备文件*/ int fd=open("/dev/fb5",O_RDWR); if(fd<0) { printf("/dev/fb5设备文件打开失败!\n"); return 0; } /*2. 读取LCD屏的参数*/ ioctl(fd,FBIOGET_FSCREENINFO,&finfo);//固定参数 printf("映射的长度:ße256e8d-437c-4a1b-a40c-ec65e12ac3ccn",finfo.smem_len); ioctl(fd,FBIOGET_VSCREENINFO,&vinfo);//可变参数,32位 printf("分辨率:%d*%d,ße256e8d-437c-4a1b-a40c-ec65e12ac3ccn",vinfo.xres,vinfo.yres,vinfo.bits_per_pixel); /*3. 映射LCD的地址到进程空间*/ lcd_mem=mmap(NULL,finfo.smem_len,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0); if(lcd_mem==NULL) { printf("lcd_mem映射失败!\n"); return -1; } //OLED清屏 ioctl(fd,OLED_ClearGRAM); /*4. 显示中文*/ ShowFont(0,0,56,font); ShowFont(56,0,56,font); ioctl(fd,OLED_RefreshGRAM); /*5. 关闭文件*/ munmap(lcd_mem,finfo.smem_len); close(fd); return 0;}
-
1. PCF8591介绍PCF8591是一个IIC总线接口的ADC/DAC转换芯片,功能比较强大,这篇文章就介绍在Linux系统里如何编写一个PCF8591的驱动,完成ADC数据采集,DAC数据输出。下面是PCF8591的介绍:PCF8591 是一个单片集成、单独供电、低功耗、8-bit CMOS数据获取器件。PCF8591 具有 4 个模拟输入、1 个模拟输出和 1个串行 I2C 总线接口。PCF8591 的 3 个地址引脚 A0, A1 和 A2 可用于硬件地址编程,允许在同个 I2C 总线上接入 8 个 PCF8591 器件,而无需额外的硬件。在 PCF8591 器件上输入输出的地址、控制和数据信号都是通过双线双向 I2C 总线以串行的方式进行传输。PCF8591 主要性能指标: ★单电源供电 ★PCF8591 的操作电压范围 2.5V-6V ★低待机电流 ★通过 I2C 总线串行输入/输出 ★PCF8591 通过 3 个硬件地址引脚寻址 ★PCF8591 的采样率由 I2C 总线速率决定 ★4 个模拟输入可编程为单端型或差分输入 ★自动增量频道选择 ★PCF8591 的模拟电压范围从 VSS 到 VDD ★PCF8591 内置跟踪保持电路 ★8-bit 逐次逼近 A/D 转换器 ★通过 1 路模拟输出实现 DAC 增益 模块功能描述: 1 模块芯片采用 PCF8951 2 模块支持外部 4 路电压输入采集(电压输入范围 0-5v) 3 模块集成光敏电阻,可以通过 AD 采集环境光强精确数值 4 模块集成热敏电阻,可以通过 AD 采集环境温度精确数值 5 模块集成 1 路 0-5V 电压输入采集(通过蓝色电位器调节输入 电压) 6 模块带电源指示灯(对模块供电后指示灯会亮) 7 模块带 DA 输出指示灯, 当模块 DA 输出接口电压达到一定值, 会点亮板上 DA 输出指示灯,电压越大,指示灯亮度越明显;2. 硬件环境介绍当前的开发板采用友善之臂Tiny4412开发板,采用三星的exynos-4412芯片,下面是开发板与PCF8591的硬件连线图:模块接口说明 当前项目采用的模块左边和右边分别外扩2路排针接口,分别说明如下: (1)AOUT 是芯片的DAC输出接口 (2)AINO 是芯片模拟输入接口 0 (3)AIN1 是芯片模拟输入接口 1 (4)AIN2 是芯片模拟输入接口 2 (5)AIN3 是芯片模拟输入接口 3(6)SCL 是IIC 时钟接口接MCU的IO口 (7)SDA 是IIC 数据接口 接MCU的 IO 口 (8)GND 是模块的地,外接MCU的GND (9)VCC 是电源接口,外接 3.3v-5v下面是PCF8591的原理图,介绍了每个引脚详细功能:3. 驱动案例代码下面是PCF8591的驱动代码,采用IIC子系统框架编程,驱动代码分为设备端、驱动端两部分。驱动框架采用杂项字符设备完成注册,给应用层提供访问的设备节点,详细的说明在代码路写了完整的注释。3.1 驱动端代码#include <linux/init.h> #include <linux/module.h> #include <linux/platform_device.h> #include <linux/i2c.h> #include <linux/interrupt.h> /*注册中断相关*/ #include <linux/irq.h> /*中断边沿类型定义*/ #include <linux/gpio.h> /*中断IO口定义*/ #include <linux/workqueue.h> /*工作队列相关*/ #include <linux/mutex.h> /*互斥信号量头文件*/ #include <linux/delay.h> #include <linux/miscdevice.h> /*杂项设备相关结构体*/ #include <linux/fs.h> /*文件操作集合头文件*/ #include <linux/uaccess.h> /*使用copy_to_user和copy_from_user*/ #define AIN0 0x40 #define AIN1 0x41 #define AIN2 0x42 #define AIN3 0x43 static struct i2c_client *PCF8591_client; /*IIC设备总线*/ /*读取PCF8591 ADC数据*/ unsigned char PCF8591_ReadADC(unsigned char ch) { return i2c_smbus_read_byte_data(PCF8591_client,ch); } static int PCF8591_open(struct inode *my_inode, struct file *my_file) { return 0; } static ssize_t PCF8591_read(struct file *my_file, char __user *buf, size_t my_len, loff_t * my_loff) { unsigned char data=PCF8591_ReadADC(AIN0); copy_to_user(buf,&data,1); data=PCF8591_ReadADC(AIN1); printk("1:Ø6a98819-66f5-4999-9e0b-c55116cb3288r\n",data); data=PCF8591_ReadADC(AIN2); printk("2:Ø6a98819-66f5-4999-9e0b-c55116cb3288r\n",data); data=PCF8591_ReadADC(AIN3); printk("3:Ø6a98819-66f5-4999-9e0b-c55116cb3288r\n",data); return 0; } static ssize_t PCF8591_write(struct file *my_file, const char __user *buf, size_t my_len, loff_t *my_loff) { //DAC输出 i2c_smbus_write_byte_data(PCF8591_client,0x40,100); return 0; } static int PCF8591_release(struct inode *my_inode, struct file *my_file) { return 0; } /*定义一个文件操作集合结构体*/ static struct file_operations ops_PCF8591={ .owner = THIS_MODULE, .read=PCF8591_read, /*读函数-被应用层read函数调用*/ .write=PCF8591_write, /*写函数-被应用层write函数调用*/ .open=PCF8591_open, /*打开函数-被应用层open函数调用*/ .release=PCF8591_release, /*释放函数*/ }; /*定义一个杂项设备结构体*/ static struct miscdevice misce_PCF8591={ .minor =MISC_DYNAMIC_MINOR, /*自动分配次设备号*/ .name = "Tiny4412_PCF8591", /*名称 在dev/目录下边可以找到*/ .fops = &ops_PCF8591, /*文件操作集合*/ }; static int i2c_probe(struct i2c_client *client, const struct i2c_device_id *device_id)//匹配成功时调用 { PCF8591_client=client; printk("<1>""驱动端IIC匹配的地址=0x%x\n",client->addr); /* 检测适配器是否支持smbus字节读写函数 */ if(i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { printk("适配器支持smbus字节读写函数\n"); } /*注册*/ misc_register(&misce_PCF8591); return 0; } static int i2c_remove(struct i2c_client *client) { misc_deregister(&misce_PCF8591);/*注销*/ printk("i2c_驱动端卸载成功!!!\n"); return 0; } /* IIC驱动端 */ static const struct i2c_device_id i2c_id[] = { {"Tiny4412_PCF8591",0},//设备端的名字为"my_PCF8591",后面的表示需要私有数据 {} }; struct i2c_driver i2c_drv = { .driver= { .name = "PCF8591", .owner = THIS_MODULE, }, .probe = i2c_probe, .remove = i2c_remove, .id_table = i2c_id, }; static int __init i2c_drv_init(void) { i2c_add_driver(&i2c_drv);//向iic总线注册一个驱动 return 0; } static void __exit i2c_drv_exit(void)//平台设备端的出口函数 { i2c_del_driver(&i2c_drv); } module_init(i2c_drv_init); module_exit(i2c_drv_exit); MODULE_LICENSE("GPL");3.2 设备端代码#include <linux/init.h> #include <linux/module.h> #include <linux/platform_device.h> #include <linux/i2c.h> /*获取总线*/ struct i2c_adapter *i2c_adap; //获取到的总线存放在这个结构体 static struct i2c_client *i2cClient = NULL; //PCF8591固定地址 b1001 //PCF8591硬件地址 b000 //组合:b1001000 = 0x48 //注意:IIC标准地址是7位 static unsigned short const i2c_addr_list[] = { 0x48, I2C_CLIENT_END };//地址队列 static int __init i2c_dev_init(void) { struct i2c_board_info i2c_info;//设备描述结构体,里面存放着欲设备的名字还有地址 i2c_adap = i2c_get_adapter(0); //获取0号总线 if(i2c_adap==NULL) { printk("PCF8591--II总线0 获取失败!!\n"); } memset(&i2c_info,0,sizeof(struct i2c_board_info));//把设备描述结构体清空结构体清空 strlcpy(i2c_info.type,"Tiny4412_PCF8591",I2C_NAME_SIZE);//把设备的名字赋值给i2c_info i2cClient = i2c_new_probed_device(i2c_adap,&i2c_info,i2c_addr_list,NULL); if(i2cClient==NULL) { printk("PCF8591 0x%x:地址不可用!!\n",i2c_addr_list[0]); } i2c_put_adapter(i2c_adap); printk("PCF8591_dev_init初始化成功!!\n"); return 0; } static void __exit i2c_dev_exit(void)//平台设备端的出口函数 { /*注销设备*/ i2c_unregister_device(i2cClient); i2c_release_client(i2cClient); printk("PCF8591_dev_exit ok!!\n"); } module_init(i2c_dev_init); module_exit(i2c_dev_exit); MODULE_LICENSE("GPL");3.3 应用层代码#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> /* PCF8591 应用层测试代码 */ int main(int argc,char **argv) { unsigned char data=0; int fp; float tmp; // tmp=5.34v 0.34 int a; int b; fp=open("/dev/Tiny4412_PCF8591",O_RDWR); if(fp<0) /*判断文件是否打开成功*/ { printf("PCF8591 driver open error!\n"); return -1; } while(1) { read(fp,&data,1); write(fp,&data,1); printf("ADC1=Ø6a98819-66f5-4999-9e0b-c55116cb3288n",data); tmp=(float)data*(5.0/255); //电压= 采集的数字量*(参考电压/分辨率); a=tmp; //a=5 tmp=5.3 b=(int)((tmp-a)*1000); //b=0.34 printf("ADC1=%d.%dV\r\n",(int)a,(int)b); sleep(1); } close(fp); return 0; }
-
1. MF-RC522模块介绍MFRC522是应用于13.56MHz非接触式通信中高集成度的读写卡芯片,针对“三表”应用推出的一款低电压、低成本、体积小的非接触式读写卡芯片,是智能仪表和便携式手持设备研发的较好选择。便携式手持设备研发的较好选择。MFRC522利用了先进的调制和解调概念,集成了在13.56MHz下所有类型的被动非接触式通信方式和协议。支持14443A兼容应答器信号。数字部分处理ISO14443A帧和错误检测。此外,还支持快速CRYPTO1加密算法,用语验证MIFARE系列产品。MFRC522支持MI FARE系列更高速的非接触式通信,双向数据传输速率高达424kbit/s。作为13.56MHz高集成度读写卡系列芯片族的新成员,MFRC522与MF RC500和MFRC530有不少相似之处,同时也具备许多特点和差异。它与主机间通信采用SPI模式,有利于减少连线,缩小PCB板体积,降低成本。淘宝上MFRC522的成品模块非常多,购买都会送几张白卡(IC卡),完成读写实验。淘宝上购买的MF-RC522模块基本是引出的SPI接口,实际上MF-RC522本身还支持IIC,UART协议,SPI相比来讲,协议更加简单,速度也快。当前我采用的就是淘宝购买一个封装好的成品模块,采用MFRC522原装芯片设计读卡电路,使用方便,成本低廉,适用于设备开发、读卡器开发等高应用的用户,需要进行射频卡终端设计/生产的用户。本模块可直接装入各种读卡器模具。模块采用电压为3.3V,通过SPI接口简单的几条线就可以直接与用户任何CPU主板相连接通信,可以保证模块稳定可靠的工作、读卡距离远。当前文章介绍如果在Linux系统下编写MF-RC522模块驱动,配合应用层,完成IC卡号读取,扇区读写,密码验证等等。当前开发板采用友善之臂Tiny4412,芯片是三星的EXYNOS4412,驱动代码没有采用SPI子系统,直接控制IO口模拟SPI时序完成与MF-RC522之间通讯。购买模块时,会送一张IC白卡和一个钥匙扣,虽然形状不一样,内部芯片型号都是属于S50卡,常用的公交车卡、地铁卡、超市会员卡等等,都是属于这种S50卡。这个洗头还有一个S70类型的卡,空间比S50大4倍。S50卡内部就是一个EEPROM空间,可以存放任何数据,空间一共分为16个扇区,每个扇区由4块(0、1、2、3)组成。实际操作时,将16个扇区分为64个块,按绝对地址编号为0-63。IC卡没有电源的,它是由IC芯片、感应天线组成,封装在一个标准的PVC卡片内,芯片及天线无任何外露部分。是世界上最近几年发展起来的一项新技术,它成功的将射频识别技术和IC卡技术结合起来,结束了无源(卡中无电源)和免接触这一难题,是电子器件领域的一大突破。卡片在一定距离范围(通常为5—10cm)靠近读写器表面,通过无线电波的传递来完成数据的读写操作。2. 硬件原理连线3. 驱动代码示例3.1 rc522.c 源代码#include <linux/init.h> #include <linux/module.h> #include <linux/ioctl.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/err.h> #include <linux/list.h> #include <linux/errno.h> #include <linux/mutex.h> #include <linux/slab.h> #include <linux/compat.h> #include <linux/spi/spi.h> #include <linux/spi/spidev.h> #include <asm/uaccess.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/delay.h> #include "rfid_rc522.h" #include <linux/miscdevice.h> #include <linux/fs.h> /*--------------------------------RC522相关操作代码---------------------------------------------*/ /* 函数功能:RC522初始化 Tiny4412硬件连接: DO--MISO :GPB_2 DI--MOSI :GPB_3 CLK-SCLK :GPB_0 CS--CS :GPB_1 RST-- :GPB_4 */ void RC522_IO_Init(void) { /*1. 注册GPIO*/ gpio_request(EXYNOS4_GPB(0), "RC522_CLK-SCLK"); gpio_request(EXYNOS4_GPB(1), "RC522_CS"); gpio_request(EXYNOS4_GPB(2), "MOSI"); gpio_request(EXYNOS4_GPB(3), "RC522_MOSI"); gpio_request(EXYNOS4_GPB(4), "RST"); /*2. 配置GPIO口模式*/ s3c_gpio_cfgpin(EXYNOS4_GPB(0), S3C_GPIO_OUTPUT); //时钟 s3c_gpio_cfgpin(EXYNOS4_GPB(1), S3C_GPIO_OUTPUT); //片选 s3c_gpio_cfgpin(EXYNOS4_GPB(2), S3C_GPIO_INPUT); //输入模式 s3c_gpio_cfgpin(EXYNOS4_GPB(3), S3C_GPIO_OUTPUT); //输出模式 s3c_gpio_cfgpin(EXYNOS4_GPB(4), S3C_GPIO_OUTPUT); //输出模式 /*3. 上拉GPIO口*/ gpio_set_value(EXYNOS4_GPB(0), 1); gpio_set_value(EXYNOS4_GPB(1), 1); gpio_set_value(EXYNOS4_GPB(3), 1); gpio_set_value(EXYNOS4_GPB(4), 1); } /* 函数功能:SPI时序读写一个字节 说 明:SPI底层时序,程序的移植接口 */ u8 RC522_SPI_ReadWriteOneByte(u8 data_tx) { u8 data_rx=0; u8 i; for(i=0;i<8;i++) { gpio_set_value(EXYNOS4_GPB(0), 0); if(data_tx&0x80)gpio_set_value(EXYNOS4_GPB(3), 1); else gpio_set_value(EXYNOS4_GPB(3), 0); data_tx<<=1; //继续发送下一个数据 gpio_set_value(EXYNOS4_GPB(0), 1); data_rx<<=1; if(gpio_get_value(EXYNOS4_GPB(2)))data_rx|=0x01; } return data_rx; } /* 功能描述:选卡读取卡存储器容量 输入参数:serNum 传入卡序列号 返 回 值:成功返回卡容量 */ u8 RC522_MFRC522_SelectTag(u8 *serNum) //读取卡存储器容量 { u8 i; u8 status; u8 size; u8 recvBits; u8 buffer[9]; buffer[0]=PICC_ANTICOLL1; //防撞码1 buffer[1]=0x70; buffer[6]=0x00; for(i=0;i<4;i++) { buffer[i+2]=*(serNum+i); //buffer[2]-buffer[5]为卡序列号 buffer[6]^=*(serNum+i); //卡校验码 } RC522_CalulateCRC(buffer,7,&buffer[7]); //buffer[7]-buffer[8]为RCR校验码 RC522_ClearBitMask(Status2Reg,0x08); status=RC522_PcdComMF522(PCD_TRANSCEIVE,buffer,9,buffer,&recvBits); if((status==MI_OK)&&(recvBits==0x18)) size=buffer[0]; else size=0; return size; } /* 延时函数,纳秒级 */ void RC522_Delay(u32 ns) { ndelay(ns); } /* 函数功能:RC522芯片初始化 */ void RC522_Init(void) { RC522_IO_Init(); //RC522初始化 RC522_PcdReset(); //复位RC522 RC522_PcdAntennaOff(); //关闭天线 msleep(2); //延时2毫秒 RC522_PcdAntennaOn(); //开启天线 M500PcdConfigISOType('A'); //设置RC632的工作方式 } /* 函数功能:复位RC522 */ void RC522_Reset(void) { RC522_PcdReset(); //复位RC522 RC522_PcdAntennaOff(); //关闭天线 msleep(2); //延时2毫秒 RC522_PcdAntennaOn(); //开启天线 } /* 功 能: 寻卡 参数说明: req_code[IN]:寻卡方式 0x52 = 寻感应区内所有符合14443A标准的卡 0x26 = 寻未进入休眠状态的卡 pTagType[OUT]:卡片类型代码 0x4400 = Mifare_UltraLight 0x0400 = Mifare_One(S50) 0x0200 = Mifare_One(S70) 0x0800 = Mifare_Pro(X) 0x4403 = Mifare_DESFire 返 回 值: 成功返回MI_OK */ char RC522_PcdRequest(u8 req_code,u8 *pTagType) { char status; u8 unLen; u8 ucComMF522Buf[MAXRLEN]; // MAXRLEN 18 RC522_ClearBitMask(Status2Reg,0x08); //清RC522寄存器位,/接收数据命令 RC522_WriteRawRC(BitFramingReg,0x07); //写RC632寄存器 RC522_SetBitMask(TxControlReg,0x03); //置RC522寄存器位 ucComMF522Buf[0]=req_code; //寻卡方式 status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,1,ucComMF522Buf,&unLen); //通过RC522和ISO14443卡通讯 if((status==MI_OK)&&(unLen==0x10)) { *pTagType=ucComMF522Buf[0]; *(pTagType+1)=ucComMF522Buf[1]; } else { status = MI_ERR; } return status; } /* 功 能: 防冲撞 参数说明: pSnr[OUT]:卡片序列号,4字节 返 回: 成功返回MI_OK */ char RC522_PcdAnticoll(u8 *pSnr) { char status; u8 i,snr_check=0; u8 unLen; u8 ucComMF522Buf[MAXRLEN]; RC522_ClearBitMask(Status2Reg,0x08); //清RC522寄存器位 RC522_WriteRawRC(BitFramingReg,0x00); //写 RC522_ClearBitMask(CollReg,0x80); //清 ucComMF522Buf[0]=PICC_ANTICOLL1; //PICC_ANTICOLL1 = 0x93 ucComMF522Buf[1]=0x20; status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,2,ucComMF522Buf,&unLen); //0x0c,通过RC522和ISO14443卡通讯 //PCD_TRANSCEIVE =发送并接收数据 //2:写入卡里的数据字节长度 //ucComMF522Buf:存放数据的地址 //unLen:从卡里读出的数据长度 if(status==MI_OK) { for(i=0;i<4;i++) { *(pSnr+i)=ucComMF522Buf[i]; //把读到的卡号赋值给pSnr snr_check^=ucComMF522Buf[i]; } if(snr_check!=ucComMF522Buf[i]) { status = MI_ERR; } } RC522_SetBitMask(CollReg,0x80); return status; } /* 功 能:选定卡片 参数说明:pSnr[IN]:卡片序列号,4字节 返 回:成功返回MI_OK */ char RC522_PcdSelect(u8 *pSnr) { char status; u8 i; u8 unLen; u8 ucComMF522Buf[MAXRLEN]; ucComMF522Buf[0]=PICC_ANTICOLL1; ucComMF522Buf[1]=0x70; ucComMF522Buf[6]=0; for(i=0;i<4;i++) { ucComMF522Buf[i+2]=*(pSnr+i); ucComMF522Buf[6]^=*(pSnr+i); } RC522_CalulateCRC(ucComMF522Buf,7,&ucComMF522Buf[7]); //用MF522计算CRC16函数,校验数据 RC522_ClearBitMask(Status2Reg,0x08); //清RC522寄存器位 status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,9,ucComMF522Buf,&unLen); if((status==MI_OK)&&(unLen==0x18))status=MI_OK; else status=MI_ERR; return status; } /* 功 能:验证卡片密码 参数说明:auth_mode[IN]: 密码验证模式 0x60 = 验证A密钥 0x61 = 验证B密钥 addr[IN]:块地址 pKey[IN]:扇区密码 pSnr[IN]:卡片序列号,4字节 返 回:成功返回MI_OK */ char RC522_PcdAuthState(u8 auth_mode,u8 addr,u8 *pKey,u8 *pSnr) { char status; u8 unLen; u8 ucComMF522Buf[MAXRLEN]; //MAXRLEN 18(数组的大小) //验证模式+块地址+扇区密码+卡序列号 ucComMF522Buf[0]=auth_mode; ucComMF522Buf[1]=addr; memcpy(&ucComMF522Buf[2],pKey,6); //拷贝,复制 memcpy(&ucComMF522Buf[8],pSnr,4); status=RC522_PcdComMF522(PCD_AUTHENT,ucComMF522Buf,12,ucComMF522Buf,&unLen); if((status!= MI_OK)||(!(RC522_ReadRawRC(Status2Reg)&0x08)))status = MI_ERR; return status; } /* 功 能:读取M1卡一块数据 参数说明: addr:块地址 p :读出的块数据,16字节 返 回:成功返回MI_OK */ char RC522_PcdRead(u8 addr,u8 *p) { char status; u8 unLen; u8 i,ucComMF522Buf[MAXRLEN]; //18 ucComMF522Buf[0]=PICC_READ; ucComMF522Buf[1]=addr; RC522_CalulateCRC(ucComMF522Buf,2,&ucComMF522Buf[2]); status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,4,ucComMF522Buf,&unLen);//通过RC522和ISO14443卡通讯 if((status==MI_OK&&(unLen==0x90))) { for(i=0;i<16;i++) { *(p +i)=ucComMF522Buf[i]; } } else { status=MI_ERR; } return status; } /* 功 能:写数据到M1卡指定块 参数说明:addr:块地址 p :向块写入的数据,16字节 返 回:成功返回MI_OK */ char RC522_PcdWrite(u8 addr,u8 *p) { char status; u8 unLen; u8 i,ucComMF522Buf[MAXRLEN]; ucComMF522Buf[0]=PICC_WRITE;// 0xA0 //写块 ucComMF522Buf[1]=addr; //块地址 RC522_CalulateCRC(ucComMF522Buf,2,&ucComMF522Buf[2]); status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,4,ucComMF522Buf,&unLen); if((status!= MI_OK)||(unLen != 4)||((ucComMF522Buf[0]&0x0F)!=0x0A)) { status = MI_ERR; } if(status==MI_OK) { for(i=0;i<16;i++)//向FIFO写16Byte数据 { ucComMF522Buf[i]=*(p +i); } RC522_CalulateCRC(ucComMF522Buf,16,&ucComMF522Buf[16]); status = RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,18,ucComMF522Buf,&unLen); if((status != MI_OK)||(unLen != 4)||((ucComMF522Buf[0]&0x0F)!=0x0A)) { status = MI_ERR; } } return status; } /* 功 能:命令卡片进入休眠状态 返 回:成功返回MI_OK */ char RC522_PcdHalt(void) { u8 status; u8 unLen; u8 ucComMF522Buf[MAXRLEN]; //MAXRLEN==18 status=status; ucComMF522Buf[0]=PICC_HALT; ucComMF522Buf[1]=0; RC522_CalulateCRC(ucComMF522Buf,2,&ucComMF522Buf[2]); status=RC522_PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,4,ucComMF522Buf,&unLen); return MI_OK; } /* 功 能:用MF522计算CRC16函数 参 数: *pIn :要读数CRC的数据 len:-数据长度 *pOut:计算的CRC结果 */ void RC522_CalulateCRC(u8 *pIn ,u8 len,u8 *pOut ) { u8 i,n; RC522_ClearBitMask(DivIrqReg,0x04); //CRCIrq = 0 RC522_WriteRawRC(CommandReg,PCD_IDLE); RC522_SetBitMask(FIFOLevelReg,0x80); //清FIFO指针 //向FIFO中写入数据 for(i=0;i<len;i++) { RC522_WriteRawRC(FIFODataReg,*(pIn +i)); //开始RCR计算 } RC522_WriteRawRC(CommandReg,PCD_CALCCRC); //等待CRC计算完成 i=0xFF; do { n=RC522_ReadRawRC(DivIrqReg); i--; } while((i!=0)&&!(n&0x04));//CRCIrq = 1 //读取CRC计算结果 pOut[0]=RC522_ReadRawRC(CRCResultRegL); pOut[1]=RC522_ReadRawRC(CRCResultRegM); } /* 功 能:复位RC522 返 回:成功返回MI_OK */ char RC522_PcdReset(void) { gpio_set_value(EXYNOS4_GPB(4), 1); //PF1写1 RC522_Delay(10); gpio_set_value(EXYNOS4_GPB(4), 0); //PF1清0 RC522_Delay(10); gpio_set_value(EXYNOS4_GPB(4), 1); //PF1写1 RC522_Delay(10); RC522_WriteRawRC(CommandReg,PCD_RESETPHASE); //写RC632寄存器,复位 RC522_WriteRawRC(CommandReg,PCD_RESETPHASE); //写RC632寄存器,复位 RC522_Delay(10); RC522_WriteRawRC(ModeReg,0x3D); //和Mifare卡通讯,CRC初始值0x6363 RC522_WriteRawRC(TReloadRegL,30); //写RC632寄存器 RC522_WriteRawRC(TReloadRegH,0); RC522_WriteRawRC(TModeReg,0x8D); RC522_WriteRawRC(TPrescalerReg,0x3E); RC522_WriteRawRC(TxAutoReg,0x40);//必须要 return MI_OK; } /* 函数功能:设置RC632的工作方式 */ char M500PcdConfigISOType(u8 type) { if(type=='A') //ISO14443_A { RC522_ClearBitMask(Status2Reg,0x08); //清RC522寄存器位 RC522_WriteRawRC(ModeReg,0x3D); //3F//CRC初始值0x6363 RC522_WriteRawRC(RxSelReg,0x86); //84 RC522_WriteRawRC(RFCfgReg,0x7F); //4F //调整卡的感应距离//RxGain = 48dB调节卡感应距离 RC522_WriteRawRC(TReloadRegL,30); //tmoLength);// TReloadVal = 'h6a =tmoLength(dec) RC522_WriteRawRC(TReloadRegH,0); RC522_WriteRawRC(TModeReg,0x8D); RC522_WriteRawRC(TPrescalerReg,0x3E); RC522_Delay(1000); RC522_PcdAntennaOn(); //开启天线 } else return 1; //失败,返回1 return MI_OK; //成功返回0 } /* 功 能:读RC632寄存器 参数说明:Address[IN]:寄存器地址 返 回:读出的值 */ u8 RC522_ReadRawRC(u8 Address) { u8 ucAddr; u8 ucResult=0; gpio_set_value(EXYNOS4_GPB(1), 0); //片选选中RC522 ucAddr=((Address<<1)&0x7E)|0x80; RC522_SPI_ReadWriteOneByte(ucAddr); //发送命令 ucResult=RC522_SPI_ReadWriteOneByte(0); //读取RC522返回的数据 gpio_set_value(EXYNOS4_GPB(1), 1); //释放片选线(PF0) return ucResult; //返回读到的数据 } /* 功 能:写RC632寄存器 参数说明:Address[IN]:寄存器地址 value[IN] :写入的值 */ void RC522_WriteRawRC(unsigned char Address,unsigned char value) { unsigned char ucAddr; gpio_set_value(EXYNOS4_GPB(1), 0); //PF0写 0 (SDA)(SPI1片选线,低电平有效) ucAddr=((Address<<1)&0x7E); RC522_SPI_ReadWriteOneByte(ucAddr); //SPI1发送一个字节 RC522_SPI_ReadWriteOneByte(value); //SPI1发送一个字节 gpio_set_value(EXYNOS4_GPB(1), 1); } /* 功 能:置RC522寄存器位 参数说明:reg[IN]:寄存器地址 mask[IN]:置位值 */ void RC522_SetBitMask(unsigned char reg,unsigned char mask) { char tmp=0x0; tmp=RC522_ReadRawRC(reg); //读RC632寄存器 RC522_WriteRawRC(reg,tmp|mask); //写RC632寄存器 } /* 功 能:清RC522寄存器位 参数说明:reg[IN]:寄存器地址 mask[IN]:清位值 */ void RC522_ClearBitMask(unsigned char reg,unsigned char mask) { char tmp=0x0; tmp=RC522_ReadRawRC(reg); //读RC632寄存器 RC522_WriteRawRC(reg,tmp&~mask); // clear bit mask } /* 功 能:通过RC522和ISO14443卡通讯 参数说明:Command[IN]:RC522命令字 pIn [IN]:通过RC522发送到卡片的数据 InLenByte[IN]:发送数据的字节长度 pOut [OUT]:接收到的卡片返回数据 *pOutLenBit[OUT]:返回数据的位长度 */ char RC522_PcdComMF522(unsigned char Command,unsigned char *pIn,unsigned char InLenByte,unsigned char *pOut,unsigned char *pOutLenBit) { char status=MI_ERR; unsigned char irqEn=0x00; unsigned char waitFor=0x00; unsigned char lastBits; unsigned char n; u16 i; switch(Command) { case PCD_AUTHENT: //验证密钥 irqEn=0x12; waitFor=0x10; break; case PCD_TRANSCEIVE: //发送并接收数据 irqEn=0x77; waitFor=0x30; break; default: break; } RC522_WriteRawRC(ComIEnReg,irqEn|0x80); RC522_ClearBitMask(ComIrqReg,0x80); //清所有中断位 RC522_WriteRawRC(CommandReg,PCD_IDLE); RC522_SetBitMask(FIFOLevelReg,0x80); //清FIFO缓存 for(i=0;i<InLenByte;i++) { RC522_WriteRawRC(FIFODataReg,pIn[i]); } RC522_WriteRawRC(CommandReg,Command); if(Command==PCD_TRANSCEIVE) { RC522_SetBitMask(BitFramingReg,0x80); //开始传送 } //有问题,下面的循环 //i = 600;//根据时钟频率调整,操作M1卡最大等待时间25ms i=2000; do { n=RC522_ReadRawRC(ComIrqReg); i--; } while((i!=0)&&!(n&0x01)&&!(n&waitFor)); RC522_ClearBitMask(BitFramingReg,0x80); if(i!=0) { if(!(RC522_ReadRawRC(ErrorReg)&0x1B)) { status=MI_OK; if(n&irqEn&0x01) { status=MI_NOTAGERR; } if(Command==PCD_TRANSCEIVE) { n=RC522_ReadRawRC(FIFOLevelReg); lastBits=RC522_ReadRawRC(ControlReg)&0x07; if(lastBits) { *pOutLenBit=(n-1)*8+lastBits; } else { *pOutLenBit=n*8; } if(n==0)n=1; if(n>MAXRLEN)n=MAXRLEN; for(i=0; i<n; i++) { pOut[i]=RC522_ReadRawRC(FIFODataReg); } } } else { status=MI_ERR; } } RC522_SetBitMask(ControlReg,0x80);// stop timer now RC522_WriteRawRC(CommandReg,PCD_IDLE); return status; } /* 函数功能:开启天线 参 数:每次启动或关闭天险发射之间应至少有1ms的间隔 */ void RC522_PcdAntennaOn(void) { unsigned char i; i=RC522_ReadRawRC(TxControlReg); if(!(i&0x03)) { RC522_SetBitMask(TxControlReg,0x03); } } /* 函数功能:关闭天线 参 数:每次启动或关闭天险发射之间应至少有1ms的间隔 */ void RC522_PcdAntennaOff(void) { RC522_ClearBitMask(TxControlReg,0x03); //清RC522寄存器位 } static void print_info(unsigned char *p,int cnt) { int i; for(i=0;i<cnt;i++) { printk("0x%X ",p[i]); } printk("\r\n"); } /********************************************** RCC522工作流程 RCC522初始化 寻卡 if(寻卡 == yes) 防冲撞 选卡 验证卡片 读卡片/写卡片 结束卡片操作 unsigned char req_code; //寻卡模式 unsigned char *pTagType; //卡类型。 参数说明: req_code[IN]:寻卡方式 0x52 = 寻感应区内所有符合14443A标准的卡 0x26 = 寻未进入休眠状态的卡 pTagType[OUT]:卡片类型代码 0x4400 = Mifare_UltraLight 0x0400 = Mifare_One(S50) 0x0200 = Mifare_One(S70) 0x0800 = Mifare_Pro(X) 0x4403 = Mifare_DESFire */ static unsigned char SN[4]; //卡号 int read_card() { unsigned char CT[2];//卡类型 unsigned char status=1; status=RC522_PcdRequest(PICC_REQIDL ,CT);//(寻卡模式,卡类型),成功返回0 if(status==MI_OK)//寻卡成功 { status=MI_ERR; status=RC522_PcdAnticoll(SN); //防冲撞,成功返回0,SN是读到卡号的地址 printk("卡类型:"); print_info(CT,2);//打印类型 printk("卡号:"); print_info(SN,4);//打印卡号 } if(status==MI_OK) { status=MI_ERR; status=RC522_PcdSelect(SN); //选定卡片,SN:卡的序列号 } return status; } /* 功能:写数据到指定块 参数: u8 addr :数据存放的地址。每个扇区的0、1、2块是存放数据。3是存放密码。 一般填:0、1、2 、4、5、6、8、9、10 数据一般格式:u8 SJ[16]={255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255}; //写入的金额; */ static unsigned char write_card_data(unsigned int m_data) { unsigned char KEY[6]={0xff,0xff,0xff,0xff,0xff,0xff}; //卡密码-初始密码--白卡的出厂密码-- unsigned char data[16]; int status=MI_ERR; data[0]=m_data>>24; data[1]=m_data>>16; data[2]=m_data>>8; data[3]=m_data; /*1. 寻卡*/ status=read_card(); /*2. 验证卡密码*/ if(status==MI_OK) { status=RC522_PcdAuthState(PICC_AUTHENT1A,3,KEY,SN); //验证卡片密码 形参参数:验证方式,块地址,密码,卡许列号 } /*3. 写数据到卡*/ if(status==MI_OK) { status=RC522_PcdWrite(2,data); //写数据到第addr块,data入的数据值。 } return status; } /* 功能:读数据到指定块 参数: unsigned char auth_mode :验证密码的类型。(分为PICC_AUTHENT1A和PICC_AUTHENT1B) unsigned char addr :数据存放的地址。每个扇区的0、1、2块是存放数据。3是存放密码。 一般填:0、1、2 、4、5、6、8、9、10 unsigned char *Src_Key :原密码 unsigned char *data :读出的数据 unsigned char *pSnr :卡号 数据一般格式:unsigned char SJ[16]={255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255}; //写入的金额; */ static unsigned char read_card_data(unsigned int *m_data) { unsigned char KEY[6]={0xff,0xff,0xff,0xff,0xff,0xff}; //卡密码-初始密码--白卡的出厂密码-- int status; unsigned char data[16]; /*1. 寻卡*/ status=read_card(); /*2. 验证卡密码*/ if(status==MI_OK) { status=RC522_PcdAuthState(PICC_AUTHENT1A,3,KEY,SN); //验证卡片密码 形参参数:验证方式,块地址,密码,卡许列号 } /*3. 读出数据*/ if(status==MI_OK) { status=RC522_PcdRead(2,data); //从第addr块读出数据值。 } if(status==MI_OK) { *m_data=data[0]<<24|data[1]<<16|data[2]<<8|data[3]; } return status; } static int tiny4412_open(struct inode *my_indoe, struct file *my_file) { printk("open ok\n"); return 0; } static int tiny4412_release(struct inode *my_indoe, struct file *my_file) { printk("open release\n"); return 0; } static ssize_t tiny4412_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff) { unsigned int data; read_card(); write_card_data(9999); if(read_card_data(&data)==MI_OK) { printk("data=Ü8433527-b0d4-461c-9b21-afd8787ab05fn",data); } return 0; } static ssize_t tiny4412_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff) { printk("open write\n"); return 0; } static struct file_operations tiny4412_fops= { .open=tiny4412_open, .release=tiny4412_release, .read=tiny4412_read, .write=tiny4412_write, }; static struct miscdevice misc={ .minor=255, .name="tiny4412_RC522", // /dev/下的名称 .fops=&tiny4412_fops, }; static int __init RC522_init(void) { /*1. 初始化GPIO口*/ RC522_Init(); /*2. 注册杂项字符设备*/ misc_register(&misc); return 0; } static void __exit RC522_exit(void) { /*释放GPIO口*/ gpio_free(EXYNOS4_GPB(0)); gpio_free(EXYNOS4_GPB(1)); gpio_free(EXYNOS4_GPB(2)); gpio_free(EXYNOS4_GPB(3)); gpio_free(EXYNOS4_GPB(4)); /*2. 注销*/ misc_deregister(&misc); printk("RC522 driver exit ok!\n"); } module_exit(RC522_exit); module_init(RC522_init); MODULE_LICENSE("GPL");3.2 rc522.h 源代码#ifndef RFID_RC522_H #define RFID_RC522_H //MF522命令字 #define PCD_IDLE 0x00 //取消当前命令 #define PCD_AUTHENT 0x0E //验证密钥 #define PCD_RECEIVE 0x08 //接收数据 #define PCD_TRANSMIT 0x04 //发送数据 #define PCD_TRANSCEIVE 0x0C //发送并接收数据 #define PCD_RESETPHASE 0x0F //复位 #define PCD_CALCCRC 0x03 //CRC计算 //Mifare_One卡片命令字 #define PICC_REQIDL 0x26 //寻天线区内未进入休眠状态,返回的是卡的类型 #define PICC_REQALL 0x52 //寻天线区内全部卡,返回的是卡的类型 #define PICC_ANTICOLL1 0x93 //防冲撞 #define PICC_ANTICOLL2 0x95 //防冲撞 #define PICC_AUTHENT1A 0x60 //验证A密钥 #define PICC_AUTHENT1B 0x61 //验证B密钥 命令认证代码 #define PICC_READ 0x30 //读块 #define PICC_WRITE 0xA0 //写块 #define PICC_DECREMENT 0xC0 //扣款 #define PICC_INCREMENT 0xC1 //充值 #define PICC_RESTORE 0xC2 //调块数据到缓冲区 #define PICC_TRANSFER 0xB0 //保存缓冲区中数据 #define PICC_HALT 0x50 //休眠 //MF522 FIFO长度定义 #define DEF_FIFO_LENGTH 64 //FIFO size=64byte #define MAXRLEN 18 //MF522寄存器定义 // PAGE 0 #define RFU00 0x00 #define CommandReg 0x01 #define ComIEnReg 0x02 #define DivlEnReg 0x03 #define ComIrqReg 0x04 #define DivIrqReg 0x05 #define ErrorReg 0x06 #define Status1Reg 0x07 #define Status2Reg 0x08 #define FIFODataReg 0x09 #define FIFOLevelReg 0x0A #define WaterLevelReg 0x0B #define ControlReg 0x0C #define BitFramingReg 0x0D #define CollReg 0x0E #define RFU0F 0x0F // PAGE 1 #define RFU10 0x10 #define ModeReg 0x11 #define TxModeReg 0x12 #define RxModeReg 0x13 #define TxControlReg 0x14 #define TxAutoReg 0x15 #define TxSelReg 0x16 #define RxSelReg 0x17 #define RxThresholdReg 0x18 #define DemodReg 0x19 #define RFU1A 0x1A #define RFU1B 0x1B #define MifareReg 0x1C #define RFU1D 0x1D #define RFU1E 0x1E #define SerialSpeedReg 0x1F // PAGE 2 #define RFU20 0x20 #define CRCResultRegM 0x21 #define CRCResultRegL 0x22 #define RFU23 0x23 #define ModWidthReg 0x24 #define RFU25 0x25 #define RFCfgReg 0x26 #define GsNReg 0x27 #define CWGsCfgReg 0x28 #define ModGsCfgReg 0x29 #define TModeReg 0x2A #define TPrescalerReg 0x2B #define TReloadRegH 0x2C #define TReloadRegL 0x2D #define TCounterValueRegH 0x2E #define TCounterValueRegL 0x2F // PAGE 3 #define RFU30 0x30 #define TestSel1Reg 0x31 #define TestSel2Reg 0x32 #define TestPinEnReg 0x33 #define TestPinValueReg 0x34 #define TestBusReg 0x35 #define AutoTestReg 0x36 #define VersionReg 0x37 #define AnalogTestReg 0x38 #define TestDAC1Reg 0x39 #define TestDAC2Reg 0x3A #define TestADCReg 0x3B #define RFU3C 0x3C #define RFU3D 0x3D #define RFU3E 0x3E #define RFU3F 0x3F //和MF522通讯时返回的错误代码 #define MI_OK 0 #define MI_NOTAGERR 1 #define MI_ERR 2 #define SHAQU1 0X01 #define KUAI4 0X04 #define KUAI7 0X07 #define REGCARD 0xa1 #define CONSUME 0xa2 #define READCARD 0xa3 #define ADDMONEY 0xa4 /* RC522各种驱动函数 */ void RC522_Init(void); //功 能:RC522射频卡模块初始化 void RC522_ClearBitMask(u8 reg,u8 mask); //功 能:清RC522寄存器位 void RC522_WriteRawRC(u8 Address, u8 value); //功 能:写RC632寄存器 void RC522_SetBitMask(u8 reg,u8 mask); //功 能:置RC522寄存器位 char RC522_PcdComMF522(u8 Command,u8*pIn,u8 InLenByte,u8*pOut,u8*pOutLenBit); //功能:通过RC522和ISO14443卡通讯 void RC522_CalulateCRC(u8 *pIn,u8 len,u8 *pOut ); //功 能:用MF522计算CRC16函数 u8 RC522_ReadRawRC(u8 Address); //功 能:读RC632寄存器 char RC522_PcdReset(void); //功 能:复位RC522 char RC522_PcdRequest(unsigned char req_code,unsigned char *pTagType);//功 能:寻卡 void RC522_PcdAntennaOn(void); //功 能:开启天线 void RC522_PcdAntennaOff(void); //功 能:关闭天线 char M500PcdConfigISOType(unsigned char type); //功 能:设置RC632的工作方式 char RC522_PcdAnticoll(unsigned char *pSnr); //功 能:防冲撞 char RC522_PcdSelect(unsigned char *pSnr); //功 能:选定卡片 char RC522_PcdAuthState(unsigned char auth_mode,unsigned char addr,unsigned char *pKey,unsigned char *pSnr);//功 能:验证卡片密码 char RC522_PcdWrite(unsigned char addr,unsigned char *pData); //功 能:写数据到M1卡一块 char RC522_PcdRead(unsigned char addr,unsigned char *pData); //功 能:读取M1卡一块数据 char RC522_PcdHalt(void); //功 能:命令卡片进入休眠状态 void RC522_Reset(void); //功 能:复位RC522 u8 RC522_MFRC522_SelectTag(u8 *serNum); //功 能:读取卡存储器容量 u8 RC522_SPI_ReadWriteOneByte(u8 tx_data); #endif
-
1. 前言VS1053是一款硬件编解码的音频芯片,提供SPI接口和IIS接口两种通信协议,这篇文章是介绍在Linux下如果模拟SPI时序来操作VS1053完成录音、播放音频歌曲功能。但是没有注册标准的音频驱动,没有对接音频框架,只是在驱动层完成VS1053的直接控制,本篇的重点主要是介绍如何初始化开发板的GPIO口,使用Linux的延时函数,模拟SPI时序,代码写了两种版本,一种是直接通过ioremap直接映射GPIO口地址,完成配置,一种是直接调用官方内核提供的库函数接口,完成GPIO口初始化,控制。当前采用的开发板是友善之臂的Tiny4412,芯片是三星的EXYNOS4412,这款芯片出来有很长一段时间了,之前用在三星的S系列手机上的,最高主频是1.5GZ,稳定推荐主频是1.4GHZ,内核是三星提供的demon,友善之臂在基础上完成了移植适配,也就是现在拿到的Tiny4412开发板内核,Linux 版本是3.5,不支持设备树。2. VS1053硬件介绍VS1053这款编码解码芯片在单片机里用的较多,性价比很高,因为支持SPI接口,所以单片机操作起来也比较容易,编码解码都是芯片内部完成,不消耗CPU资源,芯片的电压支持是3.3V。可以使用VS1053设计MP3播放器,比如:用在跑步机上听歌,用在便携式音箱里放歌,做复读机、录音笔 等等。解码的音频格式支持: MP3、OGG、WMA、WAV、MIDI、AAC、FLAC(需要加载 patch)编码的音频格式支持: WAV(PCM/IMA ADPCM)、OGG(需要加载 patch)VS1053使用的12.288M 的晶振, 在12.288MHz时钟下,最高到48000HZ的所有采样率都可以正常使用。当前我采用的VS1053是正点原子设计的完整模块,方便杜邦线与开发板进行测试。模块引出的接口功能: 这是SPI接口引脚下面是SPI接口硬件的功能描述:SPI读时序:SPI写时序:VS1053模块与单片机之间的连线图:3. 驱动代码3.1 驱动端代码#include <linux/init.h> #include <linux/module.h> #include <linux/ioctl.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/err.h> #include <linux/list.h> #include <linux/errno.h> #include <linux/mutex.h> #include <linux/slab.h> #include <linux/compat.h> #include <linux/spi/spi.h> #include <linux/spi/spidev.h> #include <asm/uaccess.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/delay.h> #include "mp3_data.h" #include <linux/miscdevice.h> /*杂项字符设备头文件*/ #define VS_WRITE_COMMAND 0x02 //写命令 #define VS_READ_COMMAND 0x03 //读命令 //VS10XX寄存器定义 #define SPI_MODE 0x00 #define SPI_STATUS 0x01 #define SPI_BASS 0x02 #define SPI_CLOCKF 0x03 #define SPI_DECODE_TIME 0x04 #define SPI_AUDATA 0x05 #define SPI_WRAM 0x06 #define SPI_WRAMADDR 0x07 #define SPI_HDAT0 0x08 #define SPI_HDAT1 0x09 #define SPI_AIADDR 0x0a #define SPI_VOL 0x0b #define SPI_AICTRL0 0x0c #define SPI_AICTRL1 0x0d #define SPI_AICTRL2 0x0e #define SPI_AICTRL3 0x0f #define SM_DIFF 0x01 #define SM_JUMP 0x02 #define SM_RESET 0x04 #define SM_OUTOFWAV 0x08 #define SM_PDOWN 0x10 #define SM_TESTS 0x20 #define SM_STREAM 0x40 #define SM_PLUSV 0x80 #define SM_DACT 0x100 #define SM_SDIORD 0x200 #define SM_SDISHARE 0x400 #define SM_SDINEW 0x800 #define SM_ADPCM 0x1000 #define SM_ADPCM_HP 0x2000 #define I2S_CONFIG 0XC040 #define GPIO_DDR 0XC017 #define GPIO_IDATA 0XC018 #define GPIO_ODATA 0XC019 /* Tiny4412与VS1053硬件连接: VCC--3V~5V GND--0V SCK---SCLK:GPB_0 SI---MOSI:GPB_3 SO---MISO:GPB_2 XCS--CS :GPB_1 DREQ-----:GPB_5 XDCS-----:GPB_4 RST------:GPB_6 */ void VS1053_Init(void); u16 VS1053_ReadReg(u8 address); //读寄存器 u16 VS1053_ReadRAM(u16 addr); //读RAM void VS1053_WriteRAM(u16 addr,u16 val); //写RAM void VS1053_WriteData(u8 data); //写数据 void VS1053_WriteCmd(u8 address,u16 data); //写命令 u8 VS1053_Reset(void); //硬复位 void VS1053_SoftReset(void); //软复位 u8 VS1053_SPI_ReadWriteByte(u8 data); //SPI接口,读写一个字节 void VS1053_SoftReset(void); //初始化VS1053 u8 VS1053_SendMusicData(u8* buf); //向VS10XX发送32字节 void VS1053_SetVol(u8 volx); //设置主音量 /* 函数功能:移植接口--SPI时序读写一个字节 函数参数:data:要写入的数据 返 回 值:读到的数据 */ u8 VS1053_SPI_ReadWriteByte(u8 tx_data) { u8 rx_data=0; u8 i; for(i=0;i<8;i++) { gpio_set_value(EXYNOS4_GPB(0), 0); if(tx_data&0x80){gpio_set_value(EXYNOS4_GPB(3), 1);} else {gpio_set_value(EXYNOS4_GPB(3), 0);} tx_data<<=1; gpio_set_value(EXYNOS4_GPB(0), 1); rx_data<<=1; if(gpio_get_value(EXYNOS4_GPB(2)))rx_data|=0x01; } return rx_data; } /* 函数功能:软复位VS10XX */ void VS1053_SoftReset(void) { u8 retry=0; while(gpio_get_value(EXYNOS4_GPB(5))==0); //等待软件复位结束 VS1053_SPI_ReadWriteByte(0Xff); //启动传输 retry=0; while(VS1053_ReadReg(SPI_MODE)!=0x0800) // 软件复位,新模式 { VS1053_WriteCmd(SPI_MODE,0x0804); // 软件复位,新模式 msleep(2);//等待至少1.35ms if(retry++>100)break; } while(gpio_get_value(EXYNOS4_GPB(5))==0); //等待软件复位结束 retry=0; while(VS1053_ReadReg(SPI_CLOCKF)!=0X9800) //设置VS10XX的时钟,3倍频 ,1.5xADD { VS1053_WriteCmd(SPI_CLOCKF,0X9800); //设置VS10XX的时钟,3倍频 ,1.5xADD if(retry++>100)break; } msleep(20); } /* 函数 功 能:硬复位MP3 函数返回值:1:复位失败;0:复位成功 */ u8 VS1053_Reset(void) { u8 retry=0; gpio_set_value(EXYNOS4_GPB(6), 0); msleep(20); gpio_set_value(EXYNOS4_GPB(4), 1);//取消数据传输 gpio_set_value(EXYNOS4_GPB(1), 1); //取消数据传输 gpio_set_value(EXYNOS4_GPB(6), 1); while(gpio_get_value(EXYNOS4_GPB(5))==0&&retry<200)//等待DREQ为高 { retry++; udelay(50); }; msleep(20); if(retry>=200)return 1; else return 0; } /* 函数功能:向VS10XX写命令 函数参数: address:命令地址 data :命令数据 */ void VS1053_WriteCmd(u8 address,u16 data) { while(gpio_get_value(EXYNOS4_GPB(5))==0); //等待空闲 gpio_set_value(EXYNOS4_GPB(4), 1); gpio_set_value(EXYNOS4_GPB(1), 0); VS1053_SPI_ReadWriteByte(VS_WRITE_COMMAND);//发送VS10XX的写命令 VS1053_SPI_ReadWriteByte(address); //地址 VS1053_SPI_ReadWriteByte(data>>8); //发送高八位 VS1053_SPI_ReadWriteByte(data); //第八位 gpio_set_value(EXYNOS4_GPB(1), 1); } /* 函数参数:向VS1053写数据 函数参数:data:要写入的数据 */ void VS1053_WriteData(u8 data) { gpio_set_value(EXYNOS4_GPB(4), 0); VS1053_SPI_ReadWriteByte(data); gpio_set_value(EXYNOS4_GPB(4), 1); } /* 函数功能:读VS1053的寄存器 函数参数:address:寄存器地址 返回值:读到的值 */ u16 VS1053_ReadReg(u8 address) { u16 temp=0; while(gpio_get_value(EXYNOS4_GPB(5))==0);//非等待空闲状态 gpio_set_value(EXYNOS4_GPB(4), 1); gpio_set_value(EXYNOS4_GPB(1), 0); VS1053_SPI_ReadWriteByte(VS_READ_COMMAND);//发送VS10XX的读命令 VS1053_SPI_ReadWriteByte(address); //地址 temp=VS1053_SPI_ReadWriteByte(0xff); //读取高字节 temp=temp<<8; temp+=VS1053_SPI_ReadWriteByte(0xff); //读取低字节 gpio_set_value(EXYNOS4_GPB(1), 1); return temp; } /* 函数功能:读取VS1053的RAM 函数参数:addr:RAM地址 返 回 值:读到的值 */ u16 VS1053_ReadRAM(u16 addr) { u16 res; VS1053_WriteCmd(SPI_WRAMADDR, addr); res=VS1053_ReadReg(SPI_WRAM); return res; } /* 函数功能:写VS1053的RAM 函数参数: addr:RAM地址 val:要写入的值 */ void VS1053_WriteRAM(u16 addr,u16 val) { VS1053_WriteCmd(SPI_WRAMADDR,addr); //写RAM地址 while(gpio_get_value(EXYNOS4_GPB(5))==0); //等待空闲 VS1053_WriteCmd(SPI_WRAM,val); //写RAM值 } /* 函数参数:发送一次音频数据,固定为32字节 返 回 值:0,发送成功 1,本次数据未成功发送 */ u8 VS1053_SendMusicData(u8* buf) { u8 n; if(gpio_get_value(EXYNOS4_GPB(5))!=0) //送数据给VS10XX { gpio_set_value(EXYNOS4_GPB(4), 0); for(n=0;n<32;n++) { VS1053_SPI_ReadWriteByte(buf[n]); } gpio_set_value(EXYNOS4_GPB(4), 1); }else return 1; return 0;//成功发送了 } /* 函数功能:设定VS1053播放的音量 函数参数:volx:音量大小(0~254) */ void VS1053_SetVol(u8 volx) { u16 volt=0; //暂存音量值 volt=254-volx; //取反一下,得到最大值,表示最大的表示 volt<<=8; volt+=254-volx; //得到音量设置后大小 VS1053_WriteCmd(SPI_VOL,volt);//设音量 } /* 函数功能:VS1053初始化 Tiny4412硬件连接: VCC--3V~5V GND--0V SCK---SCLK:GPB_0 SI---MOSI:GPB_3 SO---MISO:GPB_2 XCS--CS :GPB_1 DREQ-----:GPB_5 XDCS-----:GPB_4 RST------:GPB_6 */ void VS1053SpiInit(void) { /*1. 注册GPIO*/ gpio_request(EXYNOS4_GPB(0), "VS1053_CLK-SCLK"); gpio_request(EXYNOS4_GPB(1), "VS1053_CS"); gpio_request(EXYNOS4_GPB(2), "VS1053_MISO"); gpio_request(EXYNOS4_GPB(3), "VS1053_MOSI"); gpio_request(EXYNOS4_GPB(4), "VS1053_XDCS"); gpio_request(EXYNOS4_GPB(5), "gpio_get_value(EXYNOS4_GPB(5))"); gpio_request(EXYNOS4_GPB(6), "VS1053_RST"); /*2. 配置GPIO口模式*/ s3c_gpio_cfgpin(EXYNOS4_GPB(0), S3C_GPIO_OUTPUT); //时钟 s3c_gpio_cfgpin(EXYNOS4_GPB(1), S3C_GPIO_OUTPUT); //片选 s3c_gpio_cfgpin(EXYNOS4_GPB(2), S3C_GPIO_INPUT); //输入模式 s3c_gpio_cfgpin(EXYNOS4_GPB(3), S3C_GPIO_OUTPUT); //输出模式 s3c_gpio_cfgpin(EXYNOS4_GPB(4), S3C_GPIO_OUTPUT); //输出模式 s3c_gpio_cfgpin(EXYNOS4_GPB(5), S3C_GPIO_INPUT); //输入模式 s3c_gpio_cfgpin(EXYNOS4_GPB(6), S3C_GPIO_OUTPUT); //输出模式 /*3. 上拉GPIO口*/ gpio_set_value(EXYNOS4_GPB(0), 1); gpio_set_value(EXYNOS4_GPB(1), 1); gpio_set_value(EXYNOS4_GPB(3), 1); gpio_set_value(EXYNOS4_GPB(4), 1); gpio_set_value(EXYNOS4_GPB(6), 1); } /*****************************************************************************************************/ static int tiny4412_open(struct inode *my_inode, struct file *my_file) { printk("VS1053 open函数调用成功!\r\n"); return 0; } static int tiny4412_release(struct inode *my_inode, struct file *my_file) { printk("VS1053 release函数调用成功!\r\n"); return 0; } static u8 Music_buff[32]; static ssize_t tiny4412_write(struct file *my_file, const char __user *buf, size_t len, loff_t *loff) { if(0!=copy_from_user(Music_buff,buf,len))printk("拷贝错误!\r\n"); //每次接收32个字节数据 while(VS1053_SendMusicData(Music_buff)); //给VS10XX发送音频数据 return len; } #define VS1053_INIT_SET 188 static long tiny4412_unlocked_ioctl(struct file *my_file, unsigned int cmd, unsigned long data) { switch(cmd) { case VS1053_INIT_SET: VS1053_Reset(); //硬复位MP3 VS1053_SoftReset(); //软复位VS10XX VS1053_SetVol(250); //设置音量 printk("VS1053设置成功!\r\n"); break; } return 0; } /*文件操作集合*/ static struct file_operations tiny4412_fops= { .open=tiny4412_open, .write=tiny4412_write, .release=tiny4412_release, .unlocked_ioctl=tiny4412_unlocked_ioctl }; /* 核心结构体 */ static struct miscdevice tiny4412_misc= { .minor=MISC_DYNAMIC_MINOR, /*自动分配次设备号*/ .name="tiny4412_vs1053", /*设备文件,指定/dev/生成的文件名称*/ .fops=&tiny4412_fops }; static int __init VS1053_init(void) { VS1053SpiInit(); //初始化GPIO口 /*杂项设备注册*/ misc_register(&tiny4412_misc); return 0; } static void __exit VS1053_exit(void) { /*释放GPIO口*/ gpio_free(EXYNOS4_GPB(0)); gpio_free(EXYNOS4_GPB(1)); gpio_free(EXYNOS4_GPB(2)); gpio_free(EXYNOS4_GPB(3)); gpio_free(EXYNOS4_GPB(4)); gpio_free(EXYNOS4_GPB(5)); gpio_free(EXYNOS4_GPB(6)); /*杂项设备注销*/ misc_deregister(&tiny4412_misc); printk("VS1053 driver exit ok!\n"); } module_exit(VS1053_exit); module_init(VS1053_init); MODULE_LICENSE("GPL");3.2 应用层代码#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define VS1053_INIT_SET 188 int main(int argc,char **argv) { char buff[32]; int cnt,i=0; int vs1053_fd,file_fd; if(argc!=2) { printf("argv: ./app <mp3_file_name>\r\n"); return -1; } vs1053_fd=open("/dev/tiny4412_vs1053",O_RDWR); file_fd=open(argv[1],2); if(vs1053_fd<0||file_fd<0) /*判断文件是否打开成功*/ { printf("vs1053 driver open error!\n"); return -1; } ioctl(vs1053_fd,VS1053_INIT_SET); while(1) { cnt=read(file_fd,buff,32); write(vs1053_fd,buff,cnt); if(cnt!=32)break; i++; } close(vs1053_fd); close(file_fd); return 0; }3.3 Makefile 代码KER_DRI=/work/Tiny4412/linux-3.5/ all: make -C $(KER_DRI) M=`pwd` modules cp ./*.ko /work/rootfs/tmp/ make -C $(KER_DRI) M=`pwd` modules clean rm ./*.ko -rf arm-linux-gcc vs1053_app.c -o vs1053_app cp vs1053_app /work/rootfs/tmp/ -f obj-m +=vs1053_drv.o
-
1. W25QXX介绍W25Q64是一颗SPI接口的Flash存储芯片,是华邦W25QXX系列里的一个具体型号,这个系列里包含了W25Q16,W25Q32,W25Q64,W5Q128等等。编程代码逻辑都差不多,主要是容量的区别。本篇文章就介绍如何在Linux系统下编写W25Q64芯片的驱动,完成数据存储,W25Q64支持标准SPI总线,当前驱动程序底层的代码写了两种方式,一种是采用内核提供的SPI子系统框架,一种直接采用软件模拟SPI时序的方式驱动,具体代码在第3章贴出来了。下面是来至W25Qxx中文手册的介绍W25Q64 (64M-bit), W25Q16(16M-bit)和 W25Q32(32M-bit)是为系统提供一个最小的空间、引脚和功耗的存储器解决方案的串行 Flash 存储器。 25Q 系列比普通的串行 Flash 存储器更灵活,性能更优越。基于双倍/四倍的 SPI,它们能够可以立即完成提供数据给 RAM, 包括存储声音、文本和数据。芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于 5mA,掉电时低于 1uA。所有芯片提供标准的封装。W25Q64/16/32 由每页 256 字节组成。 每页的 256 字节用一次页编程指令即可完成。 每次可以擦除 16 页(1 个扇区)、 128 页(32KB 块)、 256 页(64KB 块)和全片擦除。W25Q64 的内存空间结构: 一页 256 字节, 4K(4096 字节)为一个扇区, 16 个扇区为 1 块, 容量为 8M 字节,共有 128 个块,2048 个扇区。W25Q64/16/32 支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、 I/O1(DO)、 I/O2(WP)和 I/O3(HOLD)。 SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率 160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上 8 位和 16 位的并行 Flash 存储器。HOLD 引脚和写保护引脚可编程写保护。此外,芯片支持 JEDEC 标准,具有唯一的 64 位识别序列号。●SPI 串行存储器系列 -W25Q64:64M 位/8M 字节 -W25Q16:16M 位/2M 字节 -W25Q32:32M 位/4M 字节 -每256字节可编程页2. 硬件环境当前测试使用的开发板采用友善之臂的Tiny4412开发板,芯片是三星的EXYNOS-4412,最高主频1.5GHZ。开发板引出了SPI的IO口,这里使用的W25Q64是外置的模块,使用杜邦线与开发板的IO口连接。开发板上引出的IO口都是5V和1.8V,为了方便供电,采用了一个USB转TTL模块提供电源,测试驱动。W25Q64模块接在开发板的SPI0接口上面的。Linux内核自带有SPI子系统的设备端示例代码:Linux 内核自带的 SPI 驱动注册示例代码: \drivers\spi\spidev.c Linux 内核自带的 SPI APP 注册示例代码: \Documentation\spi如果要使用内核自带SPI驱动,可以在内核编译时配置一下。root# make menuconfig Device Drivers ---> [*] SPI support ---> <*> Samsung S3C64XX series type SPI [*] Samsung S3C64XX Channel 0 Support.Tiny4412自带内核里的SPI设备端结构:SPI0的具体GPIO口位置:3. 案例代码3.1 模拟SPI时序-编写驱动下面是W25Q64的驱动测试代码,没有注册字符设备框架,只是在驱动的入口里测试时序是否OK,打印了ID,读写了数据进行测试。#include <linux/init.h> #include <linux/module.h> #include <linux/ioctl.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/err.h> #include <linux/list.h> #include <linux/errno.h> #include <linux/mutex.h> #include <linux/slab.h> #include <linux/compat.h> #include <linux/spi/spi.h> #include <linux/spi/spidev.h> #include <asm/uaccess.h> #include <asm/io.h> #include <linux/delay.h> /*--------------------------------W25Q64相关操作代码---------------------------------------------*/ /*定义指针,用于接收虚拟地址*/ volatile unsigned int *W25Q64_GPBCON; volatile unsigned int *W25Q64_GPBDAT; /* 函数功能:W25Q64初始化 Tiny4412硬件连接: DO--MISO :GPB_2 //输入模式 DI--MOSI :GPB_3 //输出模式 CLK-SCLK :GPB_0 //时钟 CS--CS :GPB_1 //片选 */ void W25Q64_Init(void) { /*1. 初始化GPIO*/ /*映射物理地址*/ W25Q64_GPBCON=ioremap(0x11400040,4); W25Q64_GPBDAT=ioremap(0x11400044,4); *W25Q64_GPBCON &= ~(0xf << 0 * 4);*W25Q64_GPBCON |= (0x1 << 0 * 4); *W25Q64_GPBCON &= ~(0xf << 1 * 4);*W25Q64_GPBCON |= (0x1 << 1 * 4); *W25Q64_GPBCON &= ~(0xf << 2 * 4); *W25Q64_GPBCON &= ~(0xf << 3 * 4);*W25Q64_GPBCON |= (0x1 << 3 * 4); /*2. 上拉GPIO口*/ //*W25Q64_GPBDAT &= ~(1 << 4);//输出0 *W25Q64_GPBDAT |= (1 << 0); //输出1 *W25Q64_GPBDAT |= (1 << 1); //输出1 *W25Q64_GPBDAT |= (1 << 3); //输出1 } /* 函数功能:SPI时序读写一个字节 说 明:SPI底层时序,程序的移植接口 */ u8 W25Q64_SPI_ReadWriteOneByte(u8 data_tx) { u8 data_rx=0; u8 i; for(i=0;i<8;i++) { *W25Q64_GPBDAT &= ~(1 << 0);//输出0 if(data_tx&0x80)*W25Q64_GPBDAT |= (1 << 3); //输出1 else *W25Q64_GPBDAT &= ~(1 << 3);//输出0 data_tx<<=1; //继续发送下一个数据 *W25Q64_GPBDAT |= (1 << 0); //输出1 data_rx<<=1; if((*W25Q64_GPBDAT & (1 << 2)))data_rx|=0x01; } return data_rx; } /* 函数功能:写使能 */ void W25Q64_WriteEnabled(void) { *W25Q64_GPBDAT &= ~(1 << 1); //选中W25Q64 W25Q64_SPI_ReadWriteOneByte(0x06); *W25Q64_GPBDAT |= (1 << 1); //取消选中W25Q64 } /* 函数功能:读状态 */ void W25Q64_GetBusyStat(void) { unsigned char stat=1; while(stat&0x01) //判断状态最低位 { *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x05); stat=W25Q64_SPI_ReadWriteOneByte(0xFF); //读取状态寄存器的值 *W25Q64_GPBDAT |= (1 << 1); } } /* 函数功能:读取设备ID和制造商ID W25Q64: EF16 W25QQ128:EF17 */ unsigned short W25Q64_ReadDeviceID(void) { unsigned short ID; *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x90); W25Q64_SPI_ReadWriteOneByte(0x0); W25Q64_SPI_ReadWriteOneByte(0x0); W25Q64_SPI_ReadWriteOneByte(0x0); ID=W25Q64_SPI_ReadWriteOneByte(0xFF)<<8; //制造商ID ID|=W25Q64_SPI_ReadWriteOneByte(0xFF); //设备ID *W25Q64_GPBDAT |= (1 << 1); return ID; } /* 函数功能:全片擦除 */ void W25Q64_ClearAll(void) { W25Q64_WriteEnabled(); //写使能 W25Q64_GetBusyStat(); //检测状态寄存器 *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0xC7); *W25Q64_GPBDAT |= (1 << 1); W25Q64_GetBusyStat(); //检测状态寄存器 } /* 函数功能:页编程 参 数: unsigned int addr:写入的地址 void *p:将要写入的数据 unsigned int len:写入的长度 说 明:每次最多只能写入256字节 */ void W25Q64_PageWrite(unsigned int addr,void*p,unsigned int len) { unsigned short i; unsigned char *buff=p; W25Q64_WriteEnabled(); //写使能 *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x02); W25Q64_SPI_ReadWriteOneByte(addr>>16); W25Q64_SPI_ReadWriteOneByte(addr>>8); W25Q64_SPI_ReadWriteOneByte((unsigned char)addr); for(i=0;i<len;i++) { W25Q64_SPI_ReadWriteOneByte(buff[i]); } *W25Q64_GPBDAT |= (1 << 1); W25Q64_GetBusyStat(); //检测状态寄存器 } /* 函数功能:扇区擦除 参 数: unsigned int addr:扇区的地址 说 明:一个扇区是4096字节,擦除一个扇区时间至少150ms */ void W25Q64_ClearSector(unsigned int addr) { W25Q64_WriteEnabled(); //写使能 W25Q64_GetBusyStat(); //检测状态寄存器 *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x20); W25Q64_SPI_ReadWriteOneByte(addr>>16); W25Q64_SPI_ReadWriteOneByte(addr>>8); W25Q64_SPI_ReadWriteOneByte((unsigned char)addr); *W25Q64_GPBDAT |= (1 << 1); W25Q64_GetBusyStat(); //检测状态寄存器 } /* 函数功能:数据读取 参 数: */ void W25Q64_ReadData(unsigned int addr,void *p,unsigned int len) { unsigned int i=0; unsigned char *buff=p; *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x03); W25Q64_SPI_ReadWriteOneByte(addr>>16); W25Q64_SPI_ReadWriteOneByte(addr>>8); W25Q64_SPI_ReadWriteOneByte((unsigned char)addr); for(i=0;i<len;i++) { buff[i]=W25Q64_SPI_ReadWriteOneByte(0xFF); } *W25Q64_GPBDAT |= (1 << 1); } /* 函数功能:在任意地址写入任意数据,不进行校验 参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度 */ void W25Q64_WriteDataONCheck(unsigned int addr,void *p,unsigned int len) { unsigned char *buff=p; unsigned short page_remain=256-addr%6; //当前地址开始一页剩下的空间 unsigned short remain_len; //剩余未写入的长度 if(len<page_remain) //当前这一页剩下的空间足够可以写入 { page_remain=len; } while(1) { W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; addr+=page_remain; //地址向后移动 buff+=page_remain; //地址向后移动 len-=page_remain; //长度递减 if(len>256)page_remain=256; else page_remain=len; } } /* 函数功能:在任意地址写入任意数据,对扇区进行校验 参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度 说明:一个扇区的空间4096字节 */ unsigned char W25Q64_BUFF[1024*4]; //用来检验一个扇区的数据是否需要擦除 void W25Q64_WriteData(unsigned int addr,void *p,unsigned int len) { unsigned int sector_len=4096-addr@96; //剩余空间大小 unsigned char *buff=p; unsigned int i=0; if(len<sector_len) //剩下的空间足够写 { sector_len=len; } while(1) { W25Q64_ReadData(addr,W25Q64_BUFF,sector_len); for(i=0;i<sector_len;i++) { if(W25Q64_BUFF[i]!=0xFF) { W25Q64_ClearSector(addr); //擦除扇区 break; } } W25Q64_WriteDataONCheck(addr,buff,sector_len); if(sector_len==len)break; //数据写完 buff+=sector_len; addr+=sector_len; len-=sector_len; if(len>4096) { sector_len=4096; } else { sector_len=len; } } } static int __init w25q64_init(void) { /*1. 初始化GPIO口*/ W25Q64_Init(); /*2. 打印厂商芯片ID*/ unsigned short id=W25Q64_ReadDeviceID(); printk("id=0x%X\n",id); /*3. 写入数据*/ char buff[]="W25Q64-test-123456789ABCDEFG"; W25Q64_WriteData(100,buff,strlen(buff)); printk("write-data:%s\n",buff); /*4. 读出数据*/ char buff_rx[100]; W25Q64_ReadData(100,buff_rx,strlen(buff)); printk("read-data:%s\n",buff_rx); return 0; } static void __exit w25q64_exit(void) { /*释放虚拟地址*/ iounmap(W25Q64_GPBCON); iounmap(W25Q64_GPBDAT); printk("w25q64 driver exit ok!\n"); } module_exit(w25q64_exit); module_init(w25q64_init); MODULE_LICENSE("GPL");3.2 采用SPI子系统框架-编写驱动下面代码使用SPI子系统框架编写的驱动测试代码,注册了字符设备框架,但是只是做了简单的测试,目的只是测试W25Q64是否可以正常驱动,能读写存储。#include <linux/init.h> #include <linux/module.h> #include <linux/ioctl.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/err.h> #include <linux/list.h> #include <linux/errno.h> #include <linux/mutex.h> #include <linux/slab.h> #include <linux/compat.h> #include <linux/spi/spi.h> #include <linux/spi/spidev.h> #include <asm/uaccess.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/delay.h> #include <linux/io.h> #include <linux/miscdevice.h> /*杂项字符设备头文件*/ #include <linux/fs.h> /*文件操作集合*/ #include <linux/slab.h> /*--------------------------------W25Q64相关操作代码---------------------------------------------*/ struct spi_device *w25q64_spi_Device; /* 函数功能:W25Q64初始化 Tiny4412硬件连接: DO--MISO :GPB_2 //输入模式 DI--MOSI :GPB_3 //输出模式 CLK-SCLK :GPB_0 //时钟 CS--CS :GPB_1 //片选 */ /* 函数功能:读取设备ID和制造商ID W25Q64: EF16 W25QQ128:EF17 参数:0x90表示读取ID号的指令 */ unsigned short W25Q64_ReadDeviceID(void) { /*使用硬件SPI同步读写时序*/ char tx_buf[6]={0x90,0x0,0x0,0x0,0xFF,0xFF}; char rx_buf[6]; struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=6, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); return rx_buf[4]<<8|rx_buf[5]; /*得到ID值*/ } /* 函数功能:指定位置读取指定长度的数据 参 数: 0x03 表示读取数据的指令。 */ void W25Q64_ReadData(unsigned int addr,void *p,unsigned int len) { /*使用硬件SPI同步读写时序*/ char tx_buf[4]; tx_buf[0]=0x03; //读指令 tx_buf[1]=addr>>16; //以下是地址指令 tx_buf[2]=addr>>8; tx_buf[3]=addr; spi_write(w25q64_spi_Device,tx_buf,4); spi_read(w25q64_spi_Device,p,len); } /* 函数功能:写使能 */ void W25Q64_WriteEnabled(void) { /*使用硬件SPI同步读写时序*/ char tx_buf[1]={0x06}; struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .len=1, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); } /* 函数功能:读状态 */ void W25Q64_GetBusyStat(void) { unsigned char stat=1; /*使用硬件SPI同步读写时序*/ char tx_buf[2]={0x05,0xFF}; char rx_buf[2]; while(stat&0x01) //判断状态最低位 { struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=2, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); stat=rx_buf[1]; //得到状态寄存器 } } /* 函数功能:扇区擦除 参 数: unsigned int addr:扇区的地址 说 明:一个扇区是4096字节,擦除一个扇区时间至少150ms */ void W25Q64_ClearSector(unsigned int addr) { W25Q64_WriteEnabled(); //写使能 W25Q64_GetBusyStat(); //检测状态寄存器 /*使用硬件SPI同步读写时序*/ unsigned char tx_buf[4]; tx_buf[0]=0x20; tx_buf[1]=addr>>16; tx_buf[2]=addr>>8; tx_buf[3]=addr; char rx_buf[4]; struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=4, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); W25Q64_GetBusyStat(); //检测状态寄存器 } /* 函数功能:页编程 参 数: unsigned int addr:写入的地址 void *p:将要写入的数据 unsigned int len:写入的长度 说 明:每次最多只能写入256字节 */ void W25Q64_PageWrite(unsigned int addr,void*p,unsigned int len) { unsigned short i; unsigned char *buff=p; W25Q64_WriteEnabled(); //写使能 /*使用硬件SPI同步读写时序*/ unsigned char tx_buf[4]; tx_buf[0]=0x02; //页写指令 tx_buf[1]=(addr>>16)&0xFF; //以下是地址指令 tx_buf[2]=(addr>>8)&0xFF; tx_buf[3]=(addr&0xFF); //写数据 spi_write(w25q64_spi_Device,tx_buf,4); //写数据 spi_write(w25q64_spi_Device,p,len); W25Q64_GetBusyStat(); //检测状态寄存器 } /* 函数功能:在任意地址写入任意数据,不进行校验 参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度 */ void W25Q64_WriteDataONCheck(unsigned int addr,void *p,unsigned int len) { unsigned char *buff=p; unsigned short page_remain=256-addr%6; //当前地址开始一页剩下的空间 unsigned short remain_len; //剩余未写入的长度 if(len<page_remain) //当前这一页剩下的空间足够可以写入 { page_remain=len; } while(1) { W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; addr+=page_remain; //地址向后移动 buff+=page_remain; //地址向后移动 len-=page_remain; //长度递减 if(len>256)page_remain=256; else page_remain=len; } } /* 函数功能:在任意地址写入任意数据,对扇区进行校验 参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度 说明:一个扇区的空间4096字节 */ static unsigned char W25Q64_BUFF[1024*4]; //用来检验一个扇区的数据是否需要擦除 void W25Q64_WriteData(unsigned int addr,void *p,unsigned int len) { unsigned int sector_len=4096-addr@96; //剩余空间大小 unsigned char *buff=p; unsigned int i=0; if(len<sector_len) //剩下的空间足够写 { sector_len=len; } while(1) { W25Q64_ReadData(addr,W25Q64_BUFF,sector_len); for(i=0;i<sector_len;i++) { if(W25Q64_BUFF[i]!=0xFF) { W25Q64_ClearSector(addr); //擦除扇区 break; } } W25Q64_WriteDataONCheck(addr,buff,sector_len); if(sector_len==len)break; //数据写完 buff+=sector_len; addr+=sector_len; len-=sector_len; if(len>4096) { sector_len=4096; } else { sector_len=len; } } } /* 杂项字符设备注册示例----->LED */ static int tiny4412_open(struct inode *my_inode, struct file *my_file) { return 0; } static int tiny4412_release(struct inode *my_inode, struct file *my_file) { return 0; } static ssize_t tiny4412_read(struct file *my_file, char __user *buf, size_t len, loff_t *loff) { /*2. 打印厂商芯片ID*/ unsigned short id=W25Q64_ReadDeviceID(); printk("-ID=0x%X\n",id); /*3. 写入数据*/ char buff[100]="打印厂商芯片ID打印厂商芯片ID"; W25Q64_WriteData(0,buff,100); /*4. 读出数据*/ char buff_rx[100]; W25Q64_ReadData(0,buff_rx,100); printk("Read=%s\n",buff_rx); return 0; } static ssize_t tiny4412_write(struct file *my_file, const char __user *buf, size_t len, loff_t *loff) { return 0; } /*文件操作集合*/ static struct file_operations tiny4412_fops= { .open=tiny4412_open, .read=tiny4412_read, .write=tiny4412_write, .release=tiny4412_release }; /* 核心结构体 */ static struct miscdevice tiny4412_misc= { .minor=MISC_DYNAMIC_MINOR, /*自动分配次设备号*/ .name="tiny4412_W25q64", /*设备文件,指定/dev/生成的文件名称*/ .fops=&tiny4412_fops }; static int __devinit w25q64_probe(struct spi_device *spi) { /*配置SPI模式*/ spi->bits_per_word = 8; spi->mode = SPI_MODE_0; spi->max_speed_hz=1*1000000; //1Mhz if(spi_setup(spi)<0)//配置 { printk("SPI配置失败!\n"); } /*保存指针指向*/ w25q64_spi_Device=spi; printk("w25q64 probe ok!\n"); printk("SpiNum=Þd0f8cf2-fd99-479a-8ebf-aa6de9ba02a9n",spi->dev.id); /*杂项设备注册*/ misc_register(&tiny4412_misc); return 0; } static int __devexit w25q64_remove(struct spi_device *spi) { /*杂项设备注销*/ misc_deregister(&tiny4412_misc); return 0; } static struct spi_driver w25q64_spi_driver = { .driver = { .name = "spidev", .owner =THIS_MODULE, }, .probe =w25q64_probe, .remove = __devexit_p(w25q64_remove), }; /*-------------------------------------------------------------------------*/ static int __init w25q64_init(void) { spi_register_driver(&w25q64_spi_driver); printk("w25q64 driver install ok!\n"); return 0; } static void __exit w25q64_exit(void) { spi_unregister_driver(&w25q64_spi_driver); printk("w25q64 driver exit ok!\n"); } module_exit(w25q64_exit); module_init(w25q64_init); MODULE_LICENSE("GPL");
-
1. 杂项设备注册函数这篇文章介绍,如何使用杂项设备框架编写一个简单的按键驱动,完成编写、编译、安装、测试等流程,了解一个杂项字符设备驱动的开发流程。下面是杂项字符设备的接口:struct miscdevice { int minor; /*次设备号 10 20 */ const char *name; /*设备节点的名称*/ const struct file_operations *fops; /*文件操作集合*/ struct list_head list; //链表 struct device *parent; struct device *this_device; const char *nodename; umode_t mode; }; //注册杂项字符设备 extern int misc_register(struct miscdevice * misc); //注销杂项字符设备 extern int misc_deregister(struct miscdevice *misc);按键需要将值传递给应用层,需要使用到copy_to_user函数,这个函数还有一个配对的copy_from_user,下面介绍这两个函数的详细功能和参数:#include <asm/uaccess.h> unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) 函数功能: 将驱动层数据拷贝到应用层。 函数参数: void __user *to 用户空间的地址--到哪里去 const void *from 驱动层的地址--从哪里来 unsigned long n 拷贝的大小 返回值: 0表示成功。 >0表示未拷贝成功的数量。 unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) 函数功能: 将应用层的数据拷贝到驱动层。 函数参数: void *to 驱动空间的地址--拷贝到哪里去 const void __user *from 用户空间的地址--从哪里来 unsigned long n 拷贝的大小 返回值: 0表示成功。 >0表示未拷贝成功的数量。2. 编写按键驱动使用杂项设备注册按键驱动,应用层使用read接口读取按键值。编写驱动之前需要先找到按键的原理图,找到按键接到CPU那个IO上的。然后再查阅数据手册,找到这个GPIO口的寄存器地址,寄存器的配置页面,方便初始化配置GPIO口为输入模式。2.1 按键驱动源代码#include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/io.h> #include <asm/uaccess.h> /* 按键的寄存器*/ static unsigned int *GPX3CON; static unsigned int *GPX3DAT; static int tiny4412_open(struct inode *inode, struct file *file) { printk("tiny4412_open-->ok\n"); return 0; } /*应用层的函数: int key_val; read(fd,&key_val,4) ssize_t read(int fd, void *buf, size_t count); */ static ssize_t tiny4412_read(struct file *file, char __user *buf, size_t size, loff_t *seek) { int key_val=0; if(!(*GPX3DAT&1<<2)) //判断按键是否按下 { key_val=0x1; } else if(!(*GPX3DAT&1<<3)) //判断按键是否按下 { key_val=0x2; } else if(!(*GPX3DAT&1<<4)) //判断按键是否按下 { key_val=0x3; } else if(!(*GPX3DAT&1<<5)) //判断按键是否按下 { key_val=0x4; } /*数据拷贝函数: 给应用层空间赋值--将驱动层的数据拷贝给应用层*/ /*copy_to_user(void __user *to, const void *from, unsigned long n)*/ int error; error=copy_to_user(buf,&key_val,4); if(error>0) { printk("数据拷贝失败.\n"); } return 0; } static ssize_t tiny4412_write(struct file *file, const char __user *buf, size_t size, loff_t *seek) { return 0; } static int tiny4412_release(struct inode *inode, struct file *file) { printk("tiny4412_release-->ok\n"); return 0; } static struct file_operations fops= { .open=tiny4412_open, .read=tiny4412_read, .write=tiny4412_write, .release=tiny4412_release }; /* Linux内核管理驱动---设备号 设备号是一个unsigned int 的变量--32位。 设备号=主设备号+次设备号 */ static struct miscdevice misc= { .minor = MISC_DYNAMIC_MINOR, /*次设备号填255表示自动分配 主设备号固定为10*/ .name = "tiny4412_key", /*/dev目录下文件名称*/ .fops = &fops, /*文件操作接口*/ }; static int __init tiny4412_key_init(void) { /*转换物理地址*/ GPX3CON=ioremap(0x11000C60,4); GPX3DAT=ioremap(0x11000C64,4); /*配置GPIO口模式--配置按键*/ *GPX3CON&=0xFF0000FF; /*1. 杂项设备的注册函数*/ misc_register(&misc); printk("按键: 驱动安装成功\n"); return 0; } static void __exit tiny4412_key_exit(void) { /*2. 杂项设备的注销函数*/ misc_deregister(&misc); /*取消转换*/ iounmap(GPX3CON); iounmap(GPX3DAT); printk("按键: 驱动卸载成功\n"); } module_init(tiny4412_key_init); /*驱动入口--安装驱动的时候执行*/ module_exit(tiny4412_key_exit); /*驱动出口--卸载驱动的时候执行*/ MODULE_LICENSE("GPL"); /*设置模块的许可证--GPL*/2.2 makefile文件编译驱动的makefile代码。KER_DRI=/home/wbyq/work/linux-3.5/linux-3.5 all: make -C $(KER_DRI) M=`pwd` modules cp *.ko /home/wbyq/work/rootfs/code -f make -C $(KER_DRI) M=`pwd` modules clean arm-linux-gcc app.c -o app cp app /home/wbyq/work/rootfs/code -f rm app -f obj-m += miscdev_key_drv.o2.3 应用层驱动测试代码编译完运行时,传入按键的设备节点文件.#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc,char **argv) { if(argc!=2) { printf("./app <设备节点文件>\n"); return 0; } /*1. 打开设备文件*/ int fd=open(argv[1],O_RDWR); if(fd<0) { printf("%s 设备驱动打开失败.\n",argv[1]); return 0; } /*2.读写数据*/ int key_val; while(1) { read(fd,&key_val,4);//读取按键值 if(key_val) { printf("%#x\n",key_val); } } /*3. 关闭文件*/ close(fd); return 0; }2.4 驱动安装流程[root@wbyq code]# ls tiny4412_key_drv.ko [root@wbyq code]# [root@wbyq code]# insmod tiny4412_key_drv.ko [ 173.340000] 驱动测试: 驱动安装成功 [root@wbyq code]# lsmod hello_drv 616 0 - Live 0xbf000000 (O) [root@wbyq code]# modinfo tiny4412_key_drv.ko filename: tiny4412_key_drv.ko license: GPL depends: vermagic: 3.5.0-FriendlyARM SMP preempt mod_unload ARMv7 p2v8 [root@wbyq code]# rmmod tiny4412_key_drv.ko [ 391.075000] 驱动测试: 驱动卸载成功 [root@wbyq code]#
-
一、前言Linux是一种开源操作系统, Linux 是基于 Unix 操作系统的,它提供了与 Unix 类似的文件系统、进程管理、网络连接等功能。 Linux 的核心是内核,它负责管理系统资源,如内存、处理器、磁盘空间等。内核是由内核程序员编写的,并且内核的源代码是公开的,可以被任何人修改和重新发布。 Linux 操作系统是跨平台的,它可以运行在各种不同的硬件平台上,如个人电脑、服务器、移动设备等。它也可以运行在各种不同的架构上,如 x86、ARM、PowerPC 等。 Linux 操作系统的优点非常多,其中最重要的是它是免费的,可以自由使用和修改。它还具有很高的稳定性和安全性,适用于各种类型的应用场景,包括个人电脑、服务器、移动设备等。 这篇文章整理了Linux下驱动开发的系列教程;在Linux下进行驱动开发,完全将驱动程序与应用程序隔开,中间通过C库函数以及系统调用完成驱动层和应用层的数据交换。在Linux下进行驱动开发,完全将驱动程序与应用程序隔开,中间通过C库函数以及系统调用完成驱动层和应用层的数据交换。由于Linux 是一个开源操作系统,可以移植到任意硬件平台,目前有很多物联网操作系统都基于Linux进行开发,从广义角度来看,Linux 是物联网生态系统的核心,从最小的物联网设备到边缘网关和云。最近一项由 Eclipse IoT 工作组、AGILE IoT、IEEE 和开放移动联盟赞助的在线调查发现,在物联网开发人员中,大约 72% 的受访者将 Linux 用于他们的物联网设备。其开源操作系统、可扩展性、安全特性和广泛的发行版等因素使 Linux 成为物联网开发的热门选择。二、本期系列文章 【1】嵌入式开发_(exynos4412)Tiny4412裸机开发-按键检测 https://bbs.huaweicloud.com/forum/thread-0231108714396506016-1-1.html 【2】嵌入式开发_(exynos4412)Tiny4412裸机开发-点亮LED灯 https://bbs.huaweicloud.com/forum/thread-0259108714885997023-1-1.html 【3】嵌入式Linux开发-busybox根文件系统制作 https://bbs.huaweicloud.com/forum/thread-02102108715054447017-1-1.html 【4】嵌入式Linux开发-uboot常用命令介绍(上篇) https://bbs.huaweicloud.com/forum/thread-0231108869191526022-1-1.html 【5】嵌入式Linux开发-uboot常用命令介绍(下篇) https://bbs.huaweicloud.com/forum/thread-0224108869504571029-1-1.html 【6】基于STM32+华为云IoT设计的智慧路灯(从0开始上云搭建系统框架) https://bbs.huaweicloud.com/forum/thread-0224108959170598032-1-1.html 【7】嵌入式Linux开发-编写自定义uboot命令 https://bbs.huaweicloud.com/forum/thread-0297110343079945016-1-1.html 【8】嵌入式Linux开发-根文件系统NFS网络挂载 https://bbs.huaweicloud.com/forum/thread-02101110343252006013-1-1.html 【9】嵌入式Linux开发-根文件系统本地挂载 https://bbs.huaweicloud.com/forum/thread-0279110343379535017-1-1.html 【10】嵌入式Linux下完成LCD屏文字显示(帧缓冲框架) https://bbs.huaweicloud.com/forum/thread-0282110343888980019-1-1.html 【11】Linux驱动框架与杂项字符设备框架介绍 https://bbs.huaweicloud.com/forum/thread-0219110344084488024-1-1.html 【12】Linux驱动开发-外部中断的注册使用(按键为例) https://bbs.huaweicloud.com/forum/thread-02109110344183191027-1-1.html 【13】Linux驱动开发-内核共享工作队列 https://bbs.huaweicloud.com/forum/thread-0279110344337151018-1-1.html 【14】Linux驱动开发-内核定时器 https://bbs.huaweicloud.com/forum/thread-0282110344392897020-1-1.html 【15】Linux驱动开发-编写超声波测距模块的驱动 https://bbs.huaweicloud.com/forum/thread-0297110344563810018-1-1.html
-
1. 介绍当前采用的这种超声波测距模块在各大高校实验室、毕设、课设里用的很多,原理很简单,通过声波测距,发出的声音碰到障碍物会反弹,声音在空气里传播的速度是已知的,根据时间就能计算出测量的距离。这款超声波模块内部自带了时间计算电路,型号是HC-SR04 ,它可提供 2cm-400cm 的非接触式距离感测功能,距精度可达高到 3mm; 整个模块包括了超声波发射器、 接收器与控制电路。基本工作原理:(1) 采用 IO 口 TRIG 触发测距, 给至少 10us 的高电平信号;(2) 模块自动发送 8 个 40khz 的方波, 自动检测是否有信号返回;(3) 有信号返回, 通过 IO 口 ECHO 输出一个高电平, 高电平持续的时间就是超声波从发射到返回的时间。测量距离的公式:uS/58=厘米或者 uS/148=英寸; 或是: 距离=高电平时间*声速(340M/S)/2; 建议测量周期为 60ms 以上, 以防止发射信号对回响信号的影响。下面是超声波模块与开发板的连线方式:2. 示例代码下面是超声波测距模块的驱动代码,将超声波模块的输出脚接在开发板支持中断的IO口上,配置为上升沿触发,当超声波输出脚检测到高电平就进去中断服务函数,在中断服务函数里调度工作队列,最终在工作函数里完成高电平的时间长度获取,计算测量的距离,直接在驱动代码里打印出来。下面是测量的结果:2.1 驱动代码#include <linux/kernel.h> #include <linux/module.h> #include <linux/timer.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/interrupt.h> static struct timer_list timer; static struct work_struct work; static int irq; #define TRIG EXYNOS4_GPB(7) //输出触发信号 第8个IO口 #define ECHO EXYNOS4_GPX1(0) //ECHO 回响信号输出--中断 第9个IO口 /*获取高电平持续时间--us单位*/ static u32 GetTimeH(void) { ktime_t my_time,my_time2; unsigned int i,j; my_time=ktime_get(); //获取当前时间 i=ktime_to_us(my_time); //转 us while(gpio_get_value(ECHO)){} my_time2=ktime_get(); //获取当前时间 j=ktime_to_us(my_time2); //转 us return j-i; } /* 工作函数 */ static void csb_work_func(struct work_struct *work) { u32 time=GetTimeH(); printk("厘米:%d cm\n",time/58); } /*外部中断服务函数*/ irqreturn_t csb_irq_handler_func(int irq, void *dev) { /*添加工作到工作队列*/ schedule_work(&work); return IRQ_HANDLED; } /*内核定时器中断服务函数*/ static void timer_function(unsigned long data) { static u8 i=0; mod_timer(&timer,msecs_to_jiffies(1000)+jiffies); i=!i; if(i) { gpio_set_value(TRIG,1); } else { gpio_set_value(TRIG,0); } } static int __init tiny4412_linux_csb_init(void) { /*请求GPIO口使用权*/ gpio_request(TRIG,"CSB"); /*配置GPIO引脚*/ s3c_gpio_cfgpin(TRIG,S3C_GPIO_OUTPUT); /*GPIO默认输出值*/ gpio_set_value(TRIG,0); /*初始化工作函数*/ INIT_WORK(&work,csb_work_func); /*1. 获取中断号*/ irq=gpio_to_irq(ECHO); /*2. 注册中断*/ request_irq(irq,csb_irq_handler_func,IRQF_TRIGGER_RISING,"tiny4412_csb",NULL); timer.expires=HZ+jiffies; /*单位是节拍*/ timer.function=timer_function; timer.data=666; /*1. 初始化定时器*/ init_timer(&timer); /*2. 添加定时器到内核*/ add_timer(&timer); printk("驱动测试: 驱动安装成功\n"); return 0; } static void __exit tiny4412_linux_csb_cleanup(void) { /*3. 删除定时器*/ del_timer_sync(&timer); free_irq(irq,NULL); printk("驱动测试: 驱动卸载成功\n"); } module_init(tiny4412_linux_csb_init); /*驱动入口--安装驱动的时候执行*/ module_exit(tiny4412_linux_csb_cleanup); /*驱动出口--卸载驱动的时候执行*/ MODULE_LICENSE("GPL"); /*设置模块的许可证--GPL*/2.2 MakefileKER_DRI=/home/wbyq/work/linux-3.5/linux-3.5 all: make -C $(KER_DRI) M=`pwd` modules cp *.ko /home/wbyq/work/rootfs/code -f make -C $(KER_DRI) M=`pwd` modules clean obj-m += linux_csb.o
-
1. 内核定时器介绍内核定时器是内核用来控制在未来某个时间点(基于jiffies(节拍总数))调度执行某个函数的一种机制,相关函数位于 <linux/timer.h> 和 kernel/timer.c 文件中。当内核定时器定时时间到达时,会进入用户指定的函数,相当于软中断。内核定时器注册开启后,运行一次就不会再运行(相当于自动注销),我们可以重新设置定时器的超时时间,让定时器重复运行。每当时钟中断发生时,全局变量jiffies(一个32位的unsigned long 变量)就加1,因此jiffies记录了linux系统启动后时钟中断发生的次数,驱动程序常利用jiffies来计算不同事件间的时间间隔。内核每秒钟将jiffies变量增加HZ次。因此,对于HZ值为100的系统,jiffy+1等于隔了10ms,而对于HZ为1000的系统,jiffy+1仅为1ms。内核定时器结构体: 下面列出了需要关心的成员struct timer_list { unsigned long expires; //设置超时时间,用jiffies作为基准值 void (*function)(unsigned long); //类似中断服务函数,设置定时器到时后处理的函数 unsigned long data; //中断服务函数的参数 } expires设置:以当前时间为基准加上延时时间,时间基准用jiffies变量表示,延时时间可以使用以下两个宏转换成jiffies单位。2. 内核定时器相关API函数2.1 修改定时器超时时间函数原型*int mod_timer(struct timer_list timer, unsigned long expires)函数功能修改定时器超时时间函数参数timer:对应的定时器结构体 expires:超时时间函数返回值成功返回 :修改成功的时间值函数定义文件\linux-3.5\kernel\timer.c2.2 初始化定时器函数原型#define init_timer(timer)\函数功能初始化定时器结构函数参数timer:对应的定时器结构体函数定义文件\linux-3.5\include\linux\timer.h2.3 关闭定时器函数原型int del_timer(struct timer_list *timer)函数功能关闭定时器,停用一个定时器。函数参数timer:对应的定 时器结构体函数返回值返回0:成功函数定义文件\linux-3.5\include\linux\timer.h2.4 关闭定时器函数原型int del_timer_sync(struct timer_list *timer)函数功能关闭定时器,停用一个定时器,多处理器使用。如果编内核时不支持 SMP(多处理器), del_timer_sync()和 del_timer()等价函数参数timer:对应的定时器结构体函数返回值返回0:成功函数定义文件\linux-3.5\include\linux\timer.h2.5 转换时间(微妙单位)函数原型unsigned long usecs_to_jiffies(const unsigned int m)函数功能转换时间(微妙单位),用于填充定时器结构体,设置超时时间函数参数m:要转换的时间值(微妙为单位)函数返回值成功返回转换成功的时间。用于填充定时器结构体,设置超时时间函数定义文件\linux-3.5\kernel\timer.c2.6 转换时间(毫秒为单位)函数原型unsigned long msecs_to_jiffies(const unsigned int m)函数功能转换时间(毫秒为单位),用于填充定时器结构体,设置超时时间函数参数m:要转换的时间值(毫秒为单位)函数返回值成功返回转换成功的时间。用于填充定时器结构体,设置超时时间函数定义文件\linux-3.5\kernel\timer.c将jiffies单位转为struct timespec结构体表示:Void jiffies_to_timespec(const unsigned long jiffies, struct timespec *value); 示例: jiffies_to_timespec(jiffies,&value); printk("value.ts_sec=Ýe8ee734-8505-4bee-9ce2-3283fcf6dfa1n",value.tv_sec); printk("value.tv_nsec=Ýe8ee734-8505-4bee-9ce2-3283fcf6dfa1n",value.tv_nsec);2.7 初始化定时器的结构体成员TIMER_INITIALIZER( _function, _expires, _data) 宏用于赋值定时器结构体的function、 expires、 data 和 base 成员, 这个宏的定义如下所示:(被DEFINE_TIMER宏调用)#define TIMER_INITIALIZER(_function, _expires, _data) { \ .entry = { .prev = TIMER_ENTRY_STATIC }, \ .function = (_function), \ .expires = (_expires), \ .data = (_data), \ .base = &boot_tvec_bases, \ .slack = -1, \ __TIMER_LOCKDEP_MAP_INITIALIZER( \ __FILE__ ":" __stringify(__LINE__)) \ }2.8 初始化定时器并且赋值DEFINE_TIMER( _na me , _functi o n, _e x pires, _data) 宏是定义并初始化定时器成员的“快捷方式”, 这个宏定义如下所示:/*初始化定时器,并进行赋值*/ #define DEFINE_TIMER(_name, _function, _expires, _data) \ struct timer_list _name = \ TIMER_INITIALIZER(_function, _expires, _data)2.9 定时器初始化赋值setup_timer()也可用于初始化定时器并赋值其成员, 其源代码如下://初始化定时器并进行赋值 #define setup_timer(timer, fn, data) \ do { \ static struct lock_class_key __key; \ setup_timer_key((timer), #timer, &__key, (fn), (data));\ } while (0) static inline void setup_timer_key(struct timer_list * timer, const char *name, struct lock_class_key *key, void (*function)(unsigned long), unsigned long data) { timer->function = function; timer->data = data; init_timer_key(timer, name, key); }3. 使用定时器的步骤(1) 定义定时器结构体timer_list。/*定义一个内核定时器配置结构体*/ static struct timer_list mytimer ; (2) 设置超时时间,定义定时器处理函数和传参。mytimer.expires=jiffies+ msecs_to_jiffies(1000); /*设置定时器的超时时间,1000毫秒*/ //或者 //mytimer.expires=jiffies+HZ; /*设置定时器的超时时间,1000毫秒*/ mytimer.function = time_fun; /*定时器超时的回调函数,类似中断服务函数*/ mytimer.data = 12; /*传给定时器服务函数的参数*/(3) 开启定时器。init_timer(&mytimer); /*初始化定时器*/ add_timer(&mytimer); /*启动定时器*/完整示例代码:#include <linux/kernel.h> #include <linux/module.h> #include <linux/timer.h> static struct timer_list timer; static void timer_function(unsigned long data) { printk("data=%ld\n",data); mod_timer(&timer,msecs_to_jiffies(3000)+jiffies); } static int __init tiny4412_linux_timer_init(void) { timer.expires=HZ*3+jiffies; /*单位是节拍*/ timer.function=timer_function; timer.data=666; /*1. 初始化定时器*/ init_timer(&timer); /*2. 添加定时器到内核*/ add_timer(&timer); printk("驱动测试: 驱动安装成功\n"); return 0; } static void __exit tiny4412_linux_timer_cleanup(void) { /*3. 删除定时器*/ del_timer_sync(&timer); printk("驱动测试: 驱动卸载成功\n"); } module_init(tiny4412_linux_timer_init); /*驱动入口--安装驱动的时候执行*/ module_exit(tiny4412_linux_timer_cleanup); /*驱动出口--卸载驱动的时候执行*/ MODULE_LICENSE("GPL"); /*设置模块的许可证--GPL*/4. 内核提供的延时函数Linux 内核中提供了进行纳秒、微秒和毫秒延迟。 void ndelay(unsigned long nsecs) ; void udelay(unsigned long usecs) ; void mdelay(unsigned long msecs) ; 上述延迟的实现原理本质上是忙等待,根据 CPU 频率进行一定次数的循环。在内核中,最好不要直接使用mdelay()函数, 这将无谓地耗费CPU资源。 void msleep(unsigned int millisecs) ; unsigned long msleep_interruptible(unsigned int millisecs) ; void ssleep(unsigned int seconds) ; 上述函数将使得调用它的进程睡眠参数指定的时间, msleep()、 ssleep()不能被打断,而 msleep_interruptible()则可以被打断。5. 精度较高的时间获取方式高精度定时器通常用ktime作为计时单位。 获取内核高精度时间单位: ktime_t ktime_get(void)下面是一些时间辅助函数用于计算和转换:ktime_t ktime_set(const long secs, const unsigned long nsecs); ktime_t ktime_sub(const ktime_t lhs, const ktime_t rhs); ktime_t ktime_add(const ktime_t add1, const ktime_t add2); ktime_t ktime_add_ns(const ktime_t kt, u64 nsec); ktime_t ktime_sub_ns(const ktime_t kt, u64 nsec); ktime_t timespec_to_ktime(const struct timespec ts); ktime_t timeval_to_ktime(const struct timeval tv); struct timespec ktime_to_timespec(const ktime_t kt); //转换的时间通过timespec结构体保存 struct timeval ktime_to_timeval(const ktime_t kt); //转换的时间通过timeval结构体保存 s64 ktime_to_ns(const ktime_t kt); //转换为ns单位 int ktime_equal(const ktime_t cmp1, const ktime_t cmp2); s64 ktime_to_us(const ktime_t kt); //转换为us单位 s64 ktime_to_ms(const ktime_t kt); //转换为ms单位 ktime_t ns_to_ktime(u64 ns);示例: 计算经过的一段时间static int hello_init(void) { ktime_t my_time,my_time2; unsigned int i,j; unsigned int time_cnt=0; my_time=ktime_get(); //获取当前时间 i=ktime_to_us(my_time); //转us udelay(600); //延时一段时间 my_time2=ktime_get(); //获取当前时间 j=ktime_to_us(my_time2); //转us printk("time_cnt=%ld\n",j-i); //得出之间差值,正确值为: 600 return 0; }
-
什么是内核工作队列?在linux中断编程中,需要中断程序分成中断顶部和中断底部两部分,顶部负责做中断标志,然后耗时的事情在中断底部执行。那么底部分代码实现可以通过内核工作队列实现。我们就必须先知道什么是内核工作对列。 工作队列(work queue)是另外一种将工作推后执行的形式,它和内核定时器推后的情况有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。最重要的就是工作队列允许被重新调度甚至是睡眠。如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统有默认的工作者线程,自己也可以创建自己的工作者线程。工作队列结构struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; /* 工作函数指针 */ #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };工作结构相关头文件与函数我们只需要关心一个成员函数:work_func_t func;工作函数是一个函数指针,这个成员是指向工作函数的指针;内核使用这个结构来描述一个工作,一个工作简单理解就是对应于一个函数,可以通过内核调度函数来调用work_struct中func指针所指向的函数。内核共享工作队列用法示例#include <linux/module.h> #include <linux/init.h> #include <linux/workqueue.h> typedef struct __mydat{ struct work_struct mywork; int x; int y; int z; } mydat_t; //工作服务函数 static void work_handler(struct work_struct *data) { mydat_t *p; //计算结构体首地址container_of(结构体某成员的地址, 结构体, 结构体某成员) //p = container_of(data, mydat_t, mywork); //或使用下面一条代码 p = (mydat_t *)data; printk(KERN_EMERG "data:%p,\n", data); printk(KERN_EMERG "x:%d,\n", ((mydat_t *)data)->x); printk(KERN_EMERG "y:%d,\n", ((mydat_t *)data)->y); printk(KERN_EMERG "x:%d,\n", p->x); printk(KERN_EMERG "y:%d,\n", p->y); printk(KERN_EMERG "work handler function \n"); } /* 初始化函数 */ static int __init test_init(void) { //struct work_struct work; static mydat_t work; work.x = 123; work.y = 456; printk(KERN_EMERG "&work:%p\n", &work); //把work放置内核共享工作队列中 INIT_WORK(&work.mywork, work_handler); //调用工作 schedule_work(&work.mywork); printk("test_init \n"); return 0; } /* 卸载函数 */ static void __exit test_exit(void) { printk("%s is call\r\n", __FUNCTION__); } MODULE_LICENSE("GPL"); module_init(test_init); module_exit(test_exit);自定义工作队列内核有初始化好的工作队列,因为工作队列在内核驱动中使用比较频繁,为了方便使用不用每人都去注册一个工作队列,所以内核提供了一个公共的工作队列。那么什么时候我们要自己去创建工作队列呢?共享工作队列是每人都可以使用的,那就意味着你不能一直
-
1. 外部中断介绍前面有篇文章使用杂项设备完成了按键驱动的编写,实现了按键轮询检测,通过read函数向应用层传递按键值,这篇文章使用按键为例,介绍Linux内核里中断的注册方法,使用中断的方式检测按键是否按下,中断在单片机、设备驱动开发里使用的都非常多,可以更加实时的检测到按键触发的情况。Linux内核提供了中断的注册接口:(1)注册中断头文件 include\linux\interrupt.h 定义文件 include\linux\interrupt.h 函数原型 int request_irq(unsigned int irq,/*做实参传递给中断服务函数第1个参数*/ irq_handler_t handler, /*中断服务函数指针*/ unsigned long flags, const char *name, void *dev_id); /*做实参传递给中断服务函数第2个参数*/ 函数功能 向内核注册一个中断服务函数; 当发生中断号为 irq 的中断时候,会执行 handler 指针函数。 函数参数 irq:中断编号(每个中断源有惟一的编号)。 handler:中断服务函数指针。 原型 typedef irqreturn_t (*irq_handler_t)(int, void *)。 flag:中断的标志,用来描述本中断的基本特征的。 有固定的值,由中断源的特征决定; 比如外中断有:上升沿,下降沿触发中断这类标志。 name:中断名字, 注册后会出现cat /proc/interrupts dev_id: 这个参数是传递给中断服务函数。 对共享中断来说, 这个参数一定有要; 当注销共享中断中的其中一个时, 用这个来标识要注销哪一个。 对于有惟一入口的中断,可以传递 NULL; 但是一般来说都会传递一个有意义指针,在中断程序中使用, 以方便编程。 返回值 0 表示成功 -EINVAL (无效参数22)表示中断号无效。 -EBUSY (设备或者资源忙16)表示中断已经被占用。(2)注销中断void free_irq(unsigned int irq,void * dev_id) irq: 要注销的中断号 dev_id:其实就是注册时候使用的dev参数,在共享中断必不可少,不能传递NULL。 注意:为了防止在注销时同时发生中断,调用时候,先禁止中断。(3)中断开启与关闭禁止中断 void disable_irq_nosync(unsigned int irq); void disable_irq(unsigned int irq); 参数:irq,要禁止的中断对应的编号。 注意:在中断服务程序中不能使用 disable_irq 这个函数,否则内核崩溃,可以使用 disable_irq_nosync。 disable_irq:函数调用后,函数不会马上返回,而等待中断程序执行完成才返回,在中断调用会导致死锁。 disable_irq_nosync:调用后,函数马上返回。 使能中断 void enable_irq(unsigned int irq); 参数:irq,要使能的中断对应的编号 (4)获取irq中断号int gpio_to_irq(unsigned gpio);2. 外部中断驱动编写2.1 按键原理图2.2 驱动示例代码insmod 安装驱动之后就直接注册按键中断,没有注册字符设备框架,当按键按下之后,直接在驱动层通过printk打印数据提示到终端。#include <linux/kernel.h> #include <linux/module.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> /*存放按键的信息*/ struct m_key_info { int gpio; char name[50]; int val; int irq; }; struct m_key_info key_info[]= { {EXYNOS4_GPX3(2),"key_irq_1",0x01}, {EXYNOS4_GPX3(3),"key_irq_2",0x02}, {EXYNOS4_GPX3(4),"key_irq_3",0x03}, {EXYNOS4_GPX3(5),"key_irq_4",0x04}, }; /* 中断服务函数 */ static irqreturn_t key_irq_handler(int irq, void *dev) { struct m_key_info *p=(struct m_key_info*)dev; if(gpio_get_value(p->gpio)==0) //判断按键是否按下 { printk("按键值:%#x\n",p->val); } else { printk("按键值:%#x\n",p->val|0x80); } return IRQ_HANDLED; } static int __init tiny4412_interrupt_drv_init(void) { int i; for(i=0;i<sizeof(key_info)/sizeof(key_info[0]);i++) { /*1. 获取中断号*/ key_info[i].irq=gpio_to_irq(key_info[i].gpio); /*2. 注册中断*/ if(request_irq(key_info[i].irq,key_irq_handler,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,key_info[i].name,&key_info[i])) { printk("中断号%d注册失败:%s\n",key_info[i].irq,key_info[i].name); } } printk("按键中断 驱动注册-安装成功.\n"); return 0; } static void __exit tiny4412_interrupt_drv_exit(void) { /*注销中断*/ int i=0; for(i=0;i<sizeof(key_info)/sizeof(key_info[0]);i++) { free_irq(key_info[i].irq,&key_info[i]); } printk("按键中断 驱动注销成功.\n"); } /*驱动入口*/ module_init(tiny4412_interrupt_drv_init); /*驱动出口*/ module_exit(tiny4412_interrupt_drv_exit); /*许可证*/ MODULE_LICENSE("GPL");2.3 makefile代码KER_DRI=/home/wbyq/work/linux-3.5/linux-3.5 all: make -C $(KER_DRI) M=`pwd` modules cp *.ko /home/wbyq/work/rootfs/code -f make -C $(KER_DRI) M=`pwd` modules clean obj-m += interrupt_key.o
-
1. Linux下驱动框架介绍1.1 驱动框架分类Linux下驱动框架分为3大类型:字符设备 ---------块设备 存储设备 SD 硬盘网络设备 网卡 无线 有线字符设备和块设备都会生成设备节点在/dev目录下。网络设备不会生成设备节点. 可以使用ifconfig查看字符设备标准框架详细区分:RTC设备驱动LCD屏设备驱动---帧缓冲设备框架声卡设备驱动---音频设备标准输入设备驱动—输入子系统框架......................等等内核提供的字符设备注册的方式: 原生的—最底层注册方式早期设备注册方式—linux 2.6标准设备注册方式杂项设备注册方式比如: 温度传感器、湿度传感器、光照度、门锁、LED灯、蜂鸣器 驱动都是使用字符设备框架编写1.2 驱动框架代码模板示例代码:#include <linux/kernel.h> #include <linux/module.h> static int __init tiny4412_hello_drv_init(void) { printk("Hello 驱动注册-安装成功.\n"); return 0; } static void __exit tiny4412_hello_drv_exit(void) { printk("Hello 驱动注销成功.\n"); } /*驱动入口*/ module_init(tiny4412_hello_drv_init); /*驱动出口*/ module_exit(tiny4412_hello_drv_exit); /*许可证*/ MODULE_LICENSE("GPL");1.3 Makefile示例代码KER_DRI=/home/wbyq/work/linux-3.5/linux-3.5 all: make -C $(KER_DRI) M=`pwd` modules clean: make -C $(KER_DRI) M=`pwd` modules clean obj-m += drv_hello.o编译完成之后,生成的驱动文件名称还是xxxx.ko文件。以ko为后缀。1.4 安装驱动过程[root@wbyq ]#insmod drv_hello.ko [ 435.765000] Hello 驱动注册-安装成功. [root@wbyq ]#rmmod drv_hello.ko rmmod: can't change directory to '/lib/modules': No such file or directory [root@wbyq ]#mkdir /lib/modules [root@wbyq ]#rmmod drv_hello.ko rmmod: can't change directory to '3.5.0-FriendlyARM': No such file or directory [root@wbyq ]# [root@wbyq ]# [root@wbyq ]#mkdir /lib/modules/3.5.0-FriendlyARM [root@wbyq ]#rmmod drv_hello.ko [ 1024.225000] Hello 驱动注销成功. [root@wbyq ]#insmod drv_hello.ko [ 1080.500000] Hello 驱动注册-安装成功. [root@wbyq ]#lsmod drv_hello 614 0 - Live 0xbf004000 (O) [root@wbyq ]#modinfo drv_hello.ko modinfo: can't open '/lib/modules/3.5.0-FriendlyARM/modules.dep': No such file or directory [root@wbyq ]#touch /lib/modules/3.5.0-FriendlyARM/modules.dep [root@wbyq ]#modinfo drv_hello.ko filename: drv_hello.ko license: GPL depends: vermagic: 3.5.0-FriendlyARM SMP preempt mod_unload ARMv7 p2v8 [root@wbyq ]#驱动的安装方式:动态安装. lsmod 查看动态方式安装的驱动.静态安装. 静态是固化到内核里的。2. 杂项设备框架2.1 框架结构介绍杂项字符设备的主设备号固定: 10 主设备号: 0 ~ 255次设备号范围: 0 ~ 255Linux内核寻找驱动节点是依靠设备号寻找的。设备号: 主设备号(区分类型)、次设备号(区分同类型的具体设备)主设备号: 10 ,240下面是查看串口设备节点、MMC设备节点的详细信息:下面是杂项设备的模型图:Linux下把无法分类的一些设备都归类为杂项设备,杂项设备本身就是字符设备,只是简单封装了一层,注册调用更加简单。杂项设备(misc device)是在嵌入式系统中用得比较多的一种设备驱动。在Linux内核的include\linux目录下有Miscdevice.h文件,misc设备定义及其内核提供的相关函数在这里。内核用struct miscdevice的结构体来描述杂项设备:struct miscdevice { int minor; //次设备号,杂项设备的主设备?10 const char *name; //设备的名称 const struct file_operations *fops; //文件操作 /* 下面的成员是供内核使用 ,驱动编写不需要理会 */ struct list_head list; //misc_list的链表头 struct device *parent; //父设备 struct device *this_device; //当前设备,是device_create的返回值 }; 杂项设备结构里有一个文件集合指针,当字符设备驱动安装成功之后,在应用层是open函数打开这个设备文件,会访问到驱动层里文件集合对应的函数。文件操作集合的模型图:2.2 蜂鸣器驱动示例代码这是蜂鸣器的驱动层示例代码,使用杂项设备框架编写:#include <linux/init.h> #include <linux/module.h> #include <linux/miscdevice.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <asm/io.h> static volatile unsigned int *GPD0CON=NULL; static volatile unsigned int *GPD0DAT=NULL; static int tiny4412_open(struct inode *my_indoe, struct file *my_file) { printk("open ok\n"); /*设置蜂鸣器为输出模式*/ *GPD0CON &= ~(0xf << 0 * 4); *GPD0CON |= (1 << 0 * 4); return 0; } static int tiny4412_release(struct inode *my_indoe, struct file *my_file) { printk("open release\n"); *GPD0DAT &=~(1 << 0); //关蜂鸣器 return 0; } static ssize_t tiny4412_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff) { *GPD0DAT |= (1 << 0); //开蜂鸣器 return 0; } static ssize_t tiny4412_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff) { *GPD0DAT &=~(1 << 0); //关蜂鸣器 return 0; } static struct file_operations tiny4412_fops= { .open=tiny4412_open, .release=tiny4412_release, .read=tiny4412_read, .write=tiny4412_write, }; static struct miscdevice misc={ .minor=255, .name="tiny4412_hello", // /dev/下的名称 .fops=&tiny4412_fops, }; //__init标号: 对应一段代码(汇编)--一般设置属性(指定文本段存放的位置)。 执行hello_init函数之前先执行__init static int __init hello_init(void) { /*1. 注册杂项字符设备*/ misc_register(&misc); /*映射地址*/ GPD0CON=ioremap(0x114000A0,4); GPD0DAT=ioremap(0x114000A4,4); printk("hello_init 驱动安装成功!\n"); return 0; } static void __exit hello_exit(void) { /*2. 注销*/ misc_deregister(&misc); iounmap(GPD0CON); //将映射的地址释放掉 iounmap(GPD0DAT); printk("hello_exit驱动卸载成功!\n"); } module_init(hello_init); //驱动入口。安装驱动的时候调用 module_exit(hello_exit); //驱动出口。卸载驱动的时候调用 MODULE_AUTHOR("www.edu118.com"); //声明驱动的作者 MODULE_DESCRIPTION("hello 模块测试"); //描述当前驱动功能 MODULE_LICENSE("GPL"); //驱动许可证。支持的协议GPL。应用层的代码:#include <stdio.h> int main(int argc,char**argv) { int fd; fd=open("/dev/tiny4412_hello",2); //3 0 - 1 - 2 if(fd<0) { printf("驱动打开失败!\n"); return -1; } int data1; int data2; while(1) { read(fd,&data1,4); sleep(1); write(fd,&data2,4); sleep(1); } close(fd); return 0; }2.3 运行效果
上滑加载中
推荐直播
-
空中宣讲会 2025年华为软件精英挑战赛
2025/03/10 周一 18:00-19:00
宸睿 华为云存储技术专家、ACM-ICPC WorldFinal经验 晖哥
2025华为软挑赛空中宣讲会重磅来袭!完整赛程首曝+命题天团硬核拆题+三轮幸运抽奖赢参赛助力礼包,与全国优秀高校开发者同台竞技,直通顶尖赛事起跑线!
回顾中 -
华为开发者空间玩转DeepSeek
2025/03/13 周四 19:00-20:30
马欣 华为开发者布道师
同学们,想知道如何利用华为开发者空间部署自己的DeepSeek模型吗?想了解如何用DeepSeek在云主机上探索好玩的应用吗?想探讨如何利用DeepSeek在自己的专有云主机上辅助编程吗?让我们来一场云和AI的盛宴。
即将直播 -
华为云Metastudio×DeepSeek与RAG检索优化分享
2025/03/14 周五 16:00-17:30
大海 华为云学堂技术讲师 Cocl 华为云学堂技术讲师
本次直播将带来DeepSeek数字人解决方案,以及如何使用Embedding与Rerank实现检索优化实践,为开发者与企业提供参考,助力场景落地。
去报名
热门标签