前言
来把,学编程从打印一个"hello world"开始,那学嵌入式就从点亮1个LED灯开始。
首先你得自己对Linux Driver有个基本的理解和学习,那么学到哪里可以看这个帖子跟着操作?
答:学到设备树内容前面一节就可以来这里看代码并练习了,这里点灯暂时还用不到设备树的内容。
一、前期准备
1.友善之臂Tiny4412-ARM开发板,我在咸鱼淘的,板子+7存显示屏 = 230元。
官网: 点我
资料下载:点我
2.SD卡
注意是SD卡,大的那种,不是micro SD,容量尽量16G及以上。
3.USB网卡(可选)
我的板子不带网卡,我只能外接一个,如果你不嫌麻烦,接网线也是可以的。注意USB网卡不是随便一个都能用,毕竟Linux系统,支持没那么好,我用的是TP LINK-TL-WN823N。
4.蓝牙接收器(可选)
这个也是可选的,用了蓝牙接收器方便我连接键盘,因为我用USB的方式不知道为什么,总是莫名其妙死机或者失效不起作用。
5.一台X86的Ubuntu电脑
用来交叉编译呀,在板子上编译不现实,又卡又慢,我们要做的就是在X86上编译,放到开发板上运行。后续内容我都简称这台安装了Ubuntu的PC为电脑了。
我的版本是:Ubuntu 24.02
给开发板装系统
即烧写Ubuntu系统到SD卡中,在下载的资料里,找到如下文件:
/Additional/UbuntuDesktop/ubuntu-desktop-sdcard-image-20150723.tar.gz
解压后得到:ubuntudesktop-8g.raw
这是官方打包好的现成系统镜像,烧录后可以直接插卡上电使用。
下载软件:https://rufus.ie/
插入SD卡,选择ubuntudesktop-8g.raw镜像,点击烧录即可(这个软件是在Windows系统上操作的,暂不支持Linux,你可以选择其他支持Linux的烧录软件)。
烧录完插到开发板,此时注意,开发板有开关可以选择从NAND还是SD卡启动,记得打到SD卡启动,然后上电,完成第一步。
更新开发板上的软件、系统
参照我的博客:点我
记得一定先联网,替换源后先更新,不要做别的,不然容易崩,比如插各种外设、USB设备、安装什么软件等,容易产生各种各样的错误。
使用SSH 让电脑 和 开发板进行连接
为什么?你想一下,你在电脑上编译好的程序,传到开发板了,是不是还要离开电脑,去开发板那边用键盘执行它,很麻烦对不对,那用SSH,你就可以编译也在电脑这端,执行程序也在电脑这端(SSH工具打开的窗口就像在开发板上使用Terminal一样),官方给的镜像自带了SSH,但是还需要些设置,具体连接方法请看我的博客:
点我
打通电脑和开发板之间的文件传输
上面的SSH只是让我们很方便的在电脑端远程执行开发板的程序,做一些命令操作。
但是我们要从电脑传文件到开发板吧?因为你编译好的Linux驱动模块文件要先发给开发板把?不然执行个鬼。。。
SSH干不了发送文件这件事,所以要用到nfs这个玩意儿。具体的部署方法看我的博客:
点我
配置交叉编译环境
不知道什么是交叉编译的,还是先学学交叉编译的知识把,这个Up主讲的很简单易懂@西区的故事:
点我
大白话说就是,我写了一个C程序,编译需要用gcc对把,我电脑上的Ubuntu自带了一个gcc编译器,问:我用电脑上的gcc编译器去编译这个C程序,可以在电脑上执行吗?
答:可以。
又问,发送到开发板上能执行吗?
答:不行。
Because:你用电脑的gcc编译器,编出来的文件结构是适配X86架构的,你放到ARM架构的板子上,当然执行不了,反过来也一样,ARM上的程序丢到电脑上也跑不起来。
So,我拿一个ARM架构的gcc编译器去编这个C程序,再丢到ARM开发板上不就跑起来了~
所以这种在X86电脑上去编译ARM架构程序的行为,就叫交叉编译,于是你就要开始设置环境了,我们平时编译用的是:gcc hello_world.c -o hello_world
现在你需要配置一个编译器叫:arm-linux-gcc ,官方给了,目录在这:
/Disk-A/Linux/arm-linux-gcc-4.5.1-v6-vfp-20120301.tgz
按照视频里面教学的,相应配置就行,最后你需要达到的效果是:
Terminal中输入命令:arm-linux-gcc --version
得到:
arm-linux-gcc (ctng-1.8.1-FA) 4.5.1
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
还有一点要注意,不要自己去乱下载什么最新的arm-linux-gcc编译器,通常官方给的就是比较稳定的,Linux嵌入式追求的是稳定够用,不像Windows什么都要最新的,乱用编译器可能导致程序无法执行。
测试
自己写一个hello world,编译完丢到板子上看能不能运行。
#include <stdio.h>
int main(void)
{
printf("hello world!");
return 0;
}
arm-linux-gcc hello_world.c -o hello_world
发送到arm开发板
赋予可执行权限: sudo chmod +x hello_world
执行:./hello_world
二、硬件部分
搞通上面的流程以后,你就可以开始点灯了。
个人学习这部分知识后,对于Linux和单片机的开发,感受区别最大的是权限和规范:
1.以往我们在单片机上点个灯,只需要知道GPIO对应的寄存器,配置后写入即可,但是Linux因为用户空间和内核空间隔离导致的权限问题,需要各种各样的数据传递;
2.此外,单片机写的代码,全靠程序员自己去规范,程序员有什么好的思想,就能写出什么样的代码。
而Linux驱动有自己的开发框架,你最好去遵循这一套框架,这样十分利于维护和管理。闲话少说,咱们开始。
查找板载LED的寄存器
先看硬件手册:Disk-A/Schematic-PCB/Tiny4412-1412-Schematic.pdf
由硬件手册可知,板载有4个LED灯,分别接在:
LED1:GPM4_0
LED2:GPM4_1
LED3:GPM4_2
LED4:GPM4_3
再看DataSheet:Disk-A/Datasheet/Exynos 4412 SCP_Users Manual_Ver.0.10.00_Preliminary0.pdf
找到GPM4CON寄存器和GPM4DAT寄存器;
通过阅读可以知道CON用来配置引脚模式,DAT用来决定引脚的高低电平。
他们的地址为:
GPM4CON = 0x11000000 + 0x02E0
GPM4DAT = 0x11000000 + 0x02E4
即一个基础地址加上一个偏移地址(往上往下翻就看到了)。
编译内核源码
在写代码前,我们还需要做一件事,就是编译ARM开发板对应的内核源码。
Why?
因为你在写代码的时候,得依赖很多东西,得包含很多头文件把?比如:
#include <linux/module.h>
#include ...
那这些文件从哪来?X86电脑的Ubuntu内核吗?肯定不对,我们说过,Linux嵌入式不追求新,而追求稳定,你的电脑Ubuntu一般都是比较高的版本,比如Ubuntu 24.02,内核也会很高,比如:
louis@louis-ThinkBook:~$ uname -r
6.11.0-24-generic
但是你的开发板上的可能就很低了:
t1@FriendlyARM:~$ uname -r
3.8.13.16
你要用高版本的内核编译出来的东西去低版本的板子上跑肯定不合适,说不定语法也有很多更新。所以最好的方式就是用对应版本的内核去编译驱动,然后跑在对应内核的设备上,因此有两条路可以走:
1.自己去官网下载 official website
2.用厂商提供的
先说第一种,我们知道驱动可以编译成模块,也可以静态编译到内核中,如果你想编译到内核,也没问题,问题出在内核配置上,make menuconfig会出来内核配置选项,这时候你需要根据硬件的资源特性去配置它,比如启用I2C、启动USB、去掉不需要的网卡驱动等等。。。十分十分繁琐!如果配置不好,烧录进去还容易出问题。
于是第二种的好处就体现了,厂商(卖给你开发板的商家)已经帮你根据开发板的硬件资源特性,配置好了内核各种选项,配置完后会生成.config文件,有了这个文件,内核就知道需要编什么,不需要编什么,所以你只需要把厂商给你的.config文件替换掉原本的就行。
为了更进一步方便,之前下载的资料里面不仅提供了配置文件,也提供了内核源码,目录如下:
kernel source(3.8): Disk-A/UbuntuDesktop/linux-3.8-20151029.tgz
.config: 解压上面的kernel source,然后linux-3.8/nanopc_t1_ubuntu_defconfig
所以你需要做的就是先解压kernel source,然后:
进入linux-3.8目录
cd ~/linux-3.8
mv nanopc_t1_ubuntu_defconfig .config // 把厂商提供的config文件改名成.config
然后
make menuconfig
这一步可能会出错,因为3.8内核很老了,可能缺少些依赖,却什么装什么就行。
随便浏览一下然后选择退出,保存.config文件,接着就可以开始编译了,建议用多核编译加快速度:
nproc // 先看看自己有几个核心,比如我返回的是18
make -j18 // 18改成你自己的实际情况
编译完后就做完所有的准备工作了,后续在Makefile文件里,记得配置KERNEL_DIR为linux-3.8目录。
后续基本就是代码实现了,为了内容的完整性只做简单的解释,自行看下注释。
三、代码编写
LED寄存器定义
regs.h
#pragma once
#define GPM4BASE 0x11000000
#define GPM4CON_OFFSET 0x02E0
#define GPM4DAT_OFFSET 0x02E4
#define GPM4CON (*(volatile unsigned long *)(GPM4BASE + GPM4CON_OFFSET))
#define GPM4DAT (*(volatile unsigned long *)(GPM4BASE + GPM4DAT_OFFSET))
LED私有数据定义
led_dev.h
#pragma once
// 定义一个结构体,保存板载LED的数量,这样driver会更加通用
struct led_platform_data {
int led_count;
};
为什么要定义它?
1.我们用的分离的思想写的驱动,即设备 与 驱动是一种分离的思想,我们不希望设备和驱动产生耦合,设备负责管理所有的硬件资源,驱动只负责执行操作逻辑,这样别人在拿到你的驱动后,只需要改自己的设备上的资源情况就行了。我们的板载LED有4个,那么就用这个led_count来记录灯的个数。
2.记录灯的个数以后,后续在对寄存器写值做位操作的时候,就可以限制其位移的大小了,这样不会破坏别的bit的状态。
LED设备文件
led_dev.c
#include "led_dev.h" // led设备的私有数据结构体定义
#include "regs.h" // 板载LED灯的相关寄存器定义
#include <linux/module.h> // 模块操作必要的头文件
#include <linux/platform_device.h> // 设备/驱动分离写法
/* 定义LED的设备资源包括CON、DAT两个寄存器资源 */
static struct resource led_resource[] = {
{
.start = GPM4BASE + GPM4CON_OFFSET, // CON寄存器的起始地址
.end = GPM4BASE + GPM4CON_OFFSET, // 只有一个32bit寄存器,所以和start一致
.flags = IORESOURCE_MEM, // 内存资源
.name = "LED_CON",
},
{
.start = GPM4BASE + GPM4DAT_OFFSET, // DAT寄存器起始地址
.end = GPM4BASE + GPM4DAT_OFFSET, // 同样只有1个寄存器,但是注意DAT是8bit的
.flags = IORESOURCE_MEM,
.name = "LED_DAT",
}};
static void led_dev_release(struct device *dev)
{