通过应用程序对 I.MX6U-ALPHA 开发板上的LED 灯进行开关操作。
先编写led的驱动程序,再编写应用程序。
1、驱动程序的编写
1.1、准备工作
在Ubuntu中新建2_led文件夹,输入如下命令:
cd /linux/IMX6ULL/Linux_Drivers
mkdir 2_led
cp 1_chrdevbase/ * ./2_led/
cd 2_led/
rm chrdevbase.code.workspace chrdevbaseAPP
mv chrdevbaseAPP.c ledAPP.c
mv chrdevbase.c led.c
1.2、编写框架
在vscode中打开文件夹,打开led.c文件,头文件不变,其余的都删除,自己敲写驱动框架。
可以参考Linux源码中的驱动来编写。
//led.c文件
#define LED_MAJOR 200/* 主设备号 */
#define LED_NAME "led" /* 设备名称 */
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_read(struct file *filp, chr __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, chr __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static int led_relaese(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/* 入口 */
static int __int led_init(void)
{
int ret = 0;
/* 注册驱动 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(ret < 0)
{
printk("register chrdev failed!\r\n");
return -EIO;
}
printk("led_init\r\n");//在编写框架的时候先用一个输出来验证一下
return 0;
}
/* 出口 */
static void __exit led_exit(void)
{
/* 注销设备 */
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led_exit\r\n");
}
/* 注册驱动加载和卸载 */
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yang");
框架搭建完了之后,编译.c文件,可以使用Makefile,只需要将上一节中的Makefile中的文件名字改成led.o就行。
在vscode终端中输入“make”即可编译。
测试:
(1)将生成的.ko文件拷贝到我们挂在的文件系统下面:
sudo cp led.ko /home/yang/linux/nfs/rootfs/lib/modules/4.1.15 -f
(2)在串口中挂载led.ko模块
在挂载的时候出现了下图的这种情况:
挂载之前写的驱动模块时,是正常的,然后挂载新的.ko文件会报错,输入depmod命令即可。
1.3、驱动程序编写
参考裸机程序代码,我们将初始化内容放在加载设备驱动这块代码里。
1.3.1、地址映射
MMU全称叫做 Memory Manage Unit,也就是内存管理单元。完成虚拟空间到物理空间的映射。对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间.
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。
GPIO1_IO03 引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。
如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。
//led.c文件
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 地址映射后的虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
static int __init led_init(viod)
{
/* 初始化LED灯,地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
}
static void __exit led_exit(void)
{
/* 取消地址映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03 );
iounmap(GPIO1_DR );
iounmap(GPIO1_GDIR );
}
1.3.2、 I/O 内存访问函数
在初始化了LED灯后,我们需要访问这些地址,当外部寄存器或内存映射到内存空间时,称为 I/O 内存。Linux 内核推荐使用一组操作函数来对映射后的内存进行读写操作。
/* 初始化 */
/* 时钟配置 */
val = readl(IMX6U_CCM_CCGR1 );
val &= ~(3 << 26);/* 清除以前的配置bit26,27 */
value|= 3 << 26;/* bit26 27置1 */
writel(val,IMX6U_CCM_CCGR1 );
writel(0x5, SW_MUX_GPIO1_IO03);/* 设置复用 */
writel(0x10B0, SW_PAD_GPIO1_IO03);/* 设置电气属性 */
val = readl(GPIO1_GDIR);
val |= 1<<3;//bit3置1,设置为输出
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val &= ~(1<<3);//bit3置0,打开led
writel(val, GPIO1_DR );
1.3.3、 测试
在Ubuntu中的VSCode的终端输入“make"命令进行编译。然后将生成的.ko文件复制到4.1.15文件中:
sudo cp led.ko /home/yang/linux/nfs/rootfs/lib/modules/4.1.15/ -f
此时的灯,不会亮,在串口界面,切换到4.1.15文件夹中,加载模块
modprobe led.ko //等亮
rmod led.ko //卸载灯没有灭,因为关闭的时候没有加载灯关闭的代码
在取消地址映射前面加载:灯关闭的代码
static void __exit led_exit(void)
{
int val = 0;
val = readl(GPIO1_DR);
val |= (1<<3);//bit3置1,关闭led
writel(val, GPIO1_DR );
/* 取消地址映射 */
}
2、应用程序编写
我们通过应用程序写0,1来控制灯的亮灭。
2.1、led.c编写
在led.c的函数static int led_release()添加关闭灯的代码。
static int led_release(struct inode *inode, struct file *file)
{
unsigned int val = 0;
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
return 0;
}
在led_write()中添加
int retvalue;
unsigned char databuf[1];
retvalue = copy_from_user(databuf, buf, count);
if(retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
/* 判断开灯还是关灯 */
led_switch(databuf[0]);
判断是开灯还是关灯,可以写个函数
#define LEDOFF 0
#define LEDON 1
static void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON){
val = readl(GPIO1_DR);
val &= ~(1<<3);//bit3置0,打开led
writel(val, GPIO1_DR );
}else if(sta == LEDOFF){
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
}
编译一下。
2.2、ledAPP.c编写
/*argc:应用程序参数个数
*argv[]:具体的参数内容,字符串形式
*./ledAPP <filename> <0:1> 0表示关灯,1表示开灯
*./ledAPP /dev/led 0 关灯
*./ledAPP /dev/led 1 开灯
*/
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[])
{
int fd,ret;
char *filename;
unsigned char databuf[1];//定义一个数据缓冲区
/* 检查命令是否输入正确 */
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", filename);
return -1;
}
databuf[0] = atoi(argv[2]);//将字符转换为数字
ret = write(fd, databuf,sizeof(databuf));
if(ret < 0){
printf("LED Control failed!\r\n");
close(fd);
return -1;
}
close(fd);
}
检查一下驱动程序,在led_release()中不能把灯关了,因为应用程序有关闭文件的功能,如果在led.c中把灯关了,则刚打开灯,灯就关了。
编译驱动程序、应用程序:
make
arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP
sudo cp led.ko ledAPP /home/yang/linux/nfs/rootfs/lib/modules/4.1.15/ -f
3、测试
在串口中:
进入/lib/modules/4.1.15/
/lib/modules/4.1.15/ # lsmod
/lib/modules/4.1.15/ # modprobe led.ko //加载驱动
/lib/modules/4.1.15/ # cat /proc/devices
/lib/modules/4.1.15/ # mknod /dev/led.c 200 0 //创建设备节点
/lib/modules/4.1.15/ # ls /dev/led -l
/lib/modules/4.1.15/ # ./ledAPP /dev/led 0
/lib/modules/4.1.15/ # ./ledAPP /dev/led 1
/lib/modules/4.1.15/ # rmmod led.ko