2609 发表于 2024-3-26 17:39

《原子Linux驱动开发》2-编译下载运行点灯!

<div class='showpostmsg'><p><strong><span style="font-size:24px;">前言:</span></strong></p>

<p>经过一系列的摸索,终于也是配置好了开发板的交叉编译环境,接下来我将把《原子Linux驱动开发》书上的LED驱动例程分享给大家</p>

<p><span style="font-size:24px;"><strong>介绍:</strong></span></p>

<p>从单片机转到linux,想点个灯就没有那么简单了,在单片机中就直接配置寄存器,如果用STM32的直接在cubemx中图形化配置,完成外设的初始化,然后调用对应控制GPIO的函数即可。但是在linux中就没有那么简单了。</p>

<p>在Linux中点亮一个灯的方法有很多,裸机驱动点灯,Linux下的led驱动点灯,设备树下的led点灯,pinctrl 和 gpio 子系统点灯,虽然都是点灯,但是驱动的本质还是没变,都是配置 LED 灯 所使用的 GPIO 寄存器,驱动开发方式和裸机基本没啥区别。Linux 是一个庞大而完善的系统, 尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用&ldquo;原始&rdquo;的裸机驱动开发方式,否则 就相当于你买了一辆车,结果每天推着车去上班。Linux 内核提供了 pinctrl 和 gpio 子系统用于 GPIO 驱动,随着学习的不断深入,最后我们将学习如何借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。</p>

<p>但在这之前,还是得老老实实的编写led驱动,即本书的第二章:嵌入式Linux LED灯驱动开发实验</p>

<p><strong><span style="font-size:24px;">阅读:</span></strong></p>

<p>先来学习理论部分,我们跟着目录来,学习顺序依次是了解Linux下LED灯驱动原理,硬件原理图分析,实验程序编写,运行测试</p>

<p>1)Linux下led灯驱动原理</p>

<p>Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动最 终也是对 I.MX6ULL 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux 的驱动框架。I.MX6U-ALPHA 开发板上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上, 因此本章实验的重点就是编写 Linux 下 I.MX6UL 引脚控制驱动。</p>

<p>1.地址映射</p>

<p>在编写驱动之前,我们需要先简单了解一下 MMU 这个神器,MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下: ①、完成虚拟空间到物理空间的映射。 ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。 我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了 解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于 32 位 的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的 内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间,如图 所示:</p>

<div style="text-align: center;"></div>

<div>Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚 拟 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。如果没有开启 MMU 的话 直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启 了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必 须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内 存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 ,&nbsp;定义arch/arm/include/asm/io.h 文件中,定义如下:
<pre>
<code class="language-cpp">1 #define ioremap(cookie,size) __arm_ioremap((cookie), (size),
MT_DEVICE)
2
3 void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
unsigned int mtype)
4 {
5 return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
6 }</code></pre>

<p>ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函 数有三个参数和一个返回值,这些参数和返回值的含义如下: phys_addr:要映射的物理起始地址。 size:要映射的内存空间大小。 mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、 MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。 假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应 的虚拟地址,使用如下代码即可:</p>

<pre>
<code class="language-cpp">#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); </code></pre>

<p>宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址,SW_MUX_GPIO1_IO03 是映射后 的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。 映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可。</p>

<p>iounmap 函数:卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原 型如下:</p>

<pre>
<code class="language-cpp">void iounmap (volatile void __iomem *addr)</code></pre>

<p>iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现 在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码 即可:</p>

<pre>
<code class="language-cpp">iounmap(SW_MUX_GPIO1_IO03); </code></pre>

<p>2)I/O 内存访问函数</p>

<p>这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉 及到两个概念:I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。 当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个 概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物 理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议 这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。</p>
</div>

<p>读操作函数有如下几个:</p>

<p>readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要 读取写内存地址,返回值就是读取到的数据。</p>

<p>写操作函数有如下几个:</p>

<p>writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要 写入的数值,addr 是要写入的地址。</p>

<p>3)硬件原理图</p>

<div style="text-align: center;"></div>

<div><strong><span style="font-size:24px;">程序编写:</span></strong></div>

<div>书中提到的MX6U阿尔法开发板我是没有搞来,用的是手头上这块STM32mp135dk</div>

<div>说一下点灯的思路</div>

<div>1编译:创建led文件目录编写led驱动代码文件和Makefile文件</div>

<div>2运行:在已经搭建好的交叉编译环境中make一下</div>

<div>3下载:把Ubuntu系统中编译好的程序拷贝到win主机上,在通过ssh下载到开发板usr/local目录下</div>

<div>4运行:通过ssh终端运行文件,灯亮</div>

<div>有了思路接下来我们一步一步操作就好了
<div style="text-align: center;"></div>

<p>按流程用到的命令大致就是终端中的几条,第一遍摸索的时候比较急也很乱就没有一一截图,这里偷下懒直接放出总步骤(没有cd到具体目录),记得要source一下环境。</p>

<p>led.c:</p>

<pre>
<code class="language-cpp">#include &lt;errno.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/ioctl.h&gt;
#include &lt;unistd.h&gt;

#include &lt;linux/gpio.h&gt;

int main(int argc, char **argv)
{
        struct gpiohandle_request req;
        struct gpiohandle_data data;
        char chrdev_name;
        int fd, ret;

        strcpy(chrdev_name, "/dev/gpiochip9");

        /*Open device: gpiochip9 for GPIO MCP */
        fd = open(chrdev_name, 0);
        if (fd == -1) {
                ret = -errno;
                fprintf(stderr, "Failed to open %s\n", chrdev_name);

                return ret;
        }

        /* request GPIO line: GPIO_9_15 */
        req.lineoffsets = 15;
        req.flags = GPIOHANDLE_REQUEST_OUTPUT;
        memcpy(req.default_values, &amp;data, sizeof(req.default_values));
        strcpy(req.consumer_label, "led_gpio_mcp_15");
        req.lines= 1;

        ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &amp;req);
        if (ret == -1) {
                ret = -errno;
                fprintf(stderr, "Failed to issue GET LINEHANDLE IOCTL (%d)\n",
                        ret);
        }
        if (close(fd) == -1)
                perror("Failed to close GPIO character device file");

        /*Start led blinking */
        while(1) {

                data.values = !data.values;
                ret = ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &amp;data);
                if (ret == -1) {
                        ret = -errno;
                        fprintf(stderr, "Failed to issue %s (%d)\n",
                                        "GPIOHANDLE_SET_LINE_VALUES_IOCTL", ret);
                }
                sleep(1);
        }

        /*release line */
        ret = close(req.fd);
        if (ret == -1) {
                perror("Failed to close GPIO LINEHANDLE device file");
                ret = -errno;
        }
        return ret;
}
</code></pre>

<p>Makefile代码:</p>

<pre>
<code class="language-cpp">PROG = led
SRCS = led.c
OBJS = $(SRCS:.c=.o)

CLEANFILES = $(PROG)

# Add / change option in CFLAGS if needed
# CFLAGS += &lt;new option&gt;

$(PROG):$(OBJS)
        $(CC) $(CFLAGS) -o $(PROG) $(OBJS)

.c.o:
        $(CC) $(CFLAGS) -c $&lt; -o $@

all: $(PROG)


clean:
        rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS)) *~</code></pre>

<p>这里我用FileZila实现win和Ubuntu直接的文件传输</p>

<div style="text-align: center;"></div>

<p>MobaXterm登录ssh</p>
</div>

<div>
<div style="text-align: center;">
<div style="text-align: center;"></div>

<p>&nbsp;</p>
</div>

<p>然后在终端运行命令</p>

<pre>
<code class="language-cpp">su -l weston -c "/usr/local/led"</code></pre>

<p>效果:75977efcdc025276288b581e2d4baca5</p>

<p>红灯闪烁</p>

<p>其实还有更简单的st官方的GPIOLib点灯,我也玩了下,只需要在ssh终端中输入指令即可完成点灯,它是用户层的代码可以直接通过char device控制GPIOlib,从而控制GPIO,和单片机的玩法有点像。在串口终端直接输入:</p>

<pre>
<code class="language-cpp">gpioset -cgpiochip9 14=1</code></pre>

<p>效果:f1299b59ac3c43225cf31ca98bea95f6</p>

<p>绿灯点亮</p>

<div>led闪烁程序:</div>
</div>
</div><script>                                        var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;"   style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
                                       
                                        if(parseInt(discuz_uid)==0){
                                                                                                (function($){
                                                        var postHeight = getTextHeight(400);
                                                        $(".showpostmsg").html($(".showpostmsg").html());
                                                        $(".showpostmsg").after(loginstr);
                                                        $(".showpostmsg").css({height:postHeight,overflow:"hidden"});
                                                })(jQuery);
                                        }                </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script>

freebsder 发表于 2024-3-28 15:43

<p>谢谢分享,期待更多后续</p>

2609 发表于 2024-3-29 23:36

freebsder 发表于 2024-3-28 15:43
谢谢分享,期待更多后续

<p>谢佬鼓励<img height="48" src="https://bbs.eeworld.com.cn/static/editor/plugins/hkemoji/sticker/facebook/smile.gif" width="48" /></p>
页: [1]
查看完整版本: 《原子Linux驱动开发》2-编译下载运行点灯!