Linux驱动开发笔记(三)——寄存器点灯
指南:详见《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81》41.1节内容开发板:imx6ull mini虚拟机:VMware17。
视频:第4.1讲 Linux LED灯驱动实验(直接操作寄存器)-地址映射_哔哩哔哩_bilibili
指南:详见《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81》41.1节内容
开发板:imx6ull mini
虚拟机:VMware17
ubuntu:ubuntu20.04
一、MMU
详见《指南》41.1.1部分。
MMU的两个功能:
①完成虚拟空间到物理空间的映射
②内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
主要看第①点。
一个处理器为32位,则对应的虚拟空间大小为2^32=4GB;开发板的DDR3大小为512MB,即物理空间大小为512MB。 现在让物理内存空间映射到虚拟空间,则一个物理地址就会对应多个虚拟地址。这个映射过程就是通过MMU来完成的。
Linux内核启动的时会初始化MMU以设置内存映射,设置好以后CPU访问的都是虚拟地址。
比如I.MX6ULL的GPIO1_IO03引脚的复用寄存器的地址为0X020E0068。如果没有开启MMU的话,直接向0X020E0068地址写入数据就可以配置GPIO1_IO03的复用功能。现在开启了MMU并设置了内存映射,不能直接向0X020E0068写入数据。必须使用其对应的虚拟地址。这就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。
二、映射函数
1、ioremap
// 具体实现在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/mm/nummo.c
void __iomem *__arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
return (void __iomem *)phys_addr;
}
//cookie : 要映射的物理起始地址
//size : 要映射的内存空间大小(字节)
//mtype : ioremap的类型,ioremap函数一律是MT_DEVICE。
//return : __iomem类型的指针,指向映射后的虚拟空间首地址。
// define定义在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/asm/io.h
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
用法:
假如要获取I.MX6ULL的IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器对应的虚拟地址:
#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); //IMX6ULL的一个寄存器地址是32位4字节
2、iounmap
卸载驱动时,使用iounmap释放掉ioremap的映射。
// define定义在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/asm/io.h
#define iounmap __arm_iounmap
// 具体实现在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/mm/nummo.c
void __arm_iounmap(volatile void __iomem *addr) //addr即为虚拟地址
{
}
用法:
iounmap(SW_MUX_GPIO1_IO03); //SW_MUX_GPIO1_IO03为刚才获得的虚拟地址
三、LED字符设备驱动框架
1、建立文件结构
在上次的1_chrdevbase根目录下新建新的文件夹2_led。直接把1_chrdevbase的内容复制到2_led下,新的代码直接在其基础上修改即可。
cd .../1_chrdevbase
cd .. # cd到1_chrdevbase的根目录
mkdir 2_led
cp 1_chrdevbase/* ./2_led/
cp 1_chrdevbase/.vscode/ ./2_led/ -r
cd 2_led/
rm chrdevbase.code-workspace
rm chrdevbaseAPP
mv chrdevbaseAPP.c ledAPP.c
mv chrdevbase.c led.c
ls -a # 只留下ledAPP.c led.c Makefile .vscode即可
VS打开2_led文件夹,另存为工作区

LED(工作区)
└── 2_led
├── .vscode
│ ├── c_cpp_properties.json
│ └── settings.json
├── led.c
├── led.code-workspace
├── ledAPP.c
└── Makefile
2、编写代码
2.1 修改Makefile
只修改obj-m这一行即可:
obj-m := led.o # 编译文件
2.2 修改led.c
内存访问函数
详见(09、文档教程(非常重要)/【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf)41.1.2部分。
当外部寄存器或内存映射到IO空间时,称为I/O端口。 当外部寄存器或内存映射到内存空间时,称为I/O内存。但是ARM没有I/O空间这个概念,只有I/O内存。
使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
// 读操作函数
// addr: 虚拟地址
// return: 读取到的数据
u8 readb(const volatile void __iomem *addr) // 8 bit
u16 readw(const volatile void __iomem *addr) // 16 bit
u32 readl(const volatile void __iomem *addr) // 32 bit
// 写操作函数
// value: 要写入的数据
// addr: 虚拟地址
void writeb(u8 value, volatile void __iomem *addr) // 8 bit
void writew(u16 value, volatile void __iomem *addr) // 16 bit
void writel(u32 value, volatile void __iomem *addr) // 32 bit
寄存器配置
寄存器配置详见(07、I.MX6U参考资料/02、I.MX6ULL芯片资料/IMX6ULL参考手册.pdf)
其中CCM_CCGR1在18.6.24
GPIOx_GDIR在28.5.2
GPIOx_DR在28.5.1

要使能GPIO1的时钟,需要将CCM_CCGR1寄存器的bit26、27置为1

要使能GPIO1_OI03为output,需要将GPIO_GDIR寄存器bit3置为1

要控制GPIO1_OI03输出高电平则DR的bit3置1,输出低电平则DR的bit3置0。低电平则亮灯
综上,配置代码为:
/* 地址映射 */
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);
/* 初始化 */
val = readl(IMX6U_CCM_CCGR1); // 32bit读取函数
val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。 其中3的二进制为11,~为取反操作。
val |= 3 << 26; // 将26、27位置为1
writel(val, IMX6U_CCM_CCGR1); // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。
writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性
val = readl(GPIO1_GDIR); // 32bit读取函数
val |= 1 << 3; // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
writel(val, GPIO1_GDIR);
/* 开灯 */
val = readl(GPIO1_DR); // 32bit读取函数
val &= ~(1 << 3); // 将bit3置为低电平,打开LED
writel(val, GPIO1_DR);
led.c完整代码
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include <linux/io.h>
#define LED_MAJOR 200
#define LED_NAME "led"
/* 寄存器物理地址 */
#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 led_open(struct inode * inode, struct file *filp){
return 0;
}
static int led_release(struct inode * inode, struct file *filp){
return 0;
}
static ssize_t led_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos){
return 0;
}
/* 字符设备操作集合 */
static const struct file_operations led_fope = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write
};
/* 入口 */
static int __init led_init(void){
int ret = 0;
int val = 0;
/* 地址映射 */
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);
/* 初始化 */
val = readl(IMX6U_CCM_CCGR1); // 32bit读取函数
val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。 其中3的二进制为11,~为取反操作。
val |= 3 << 26; // 将26、27位置为1
writel(val, IMX6U_CCM_CCGR1); // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。
writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性
val = readl(GPIO1_GDIR); // 32bit读取函数
val |= 1 << 3; // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
writel(val, GPIO1_GDIR);
/* 开灯 */
val = readl(GPIO1_DR); // 32bit读取函数
val &= ~(1 << 3); // 将bit3置为低电平,打开LED
writel(val, GPIO1_DR);
/* 注册设备 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fope);
if(ret<0){
printk("register chrdev failed!\r\n");
return -EIO;
}
printk("led init\r\n");
return 0;
}
/* 出口 */
static void __exit led_exit(void){
/* 关灯 */
val = readl(GPIO1_DR); // 32bit读取函数
val |= (1 << 3); // 将bit3置为高电平,关闭LED
writel(val, GPIO1_DR);
/* 取消地址映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* 注销设备 */
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led exit\r\n");
}
/* 驱动加载卸载*/
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
然后编译复制挂载即可:
# vscode终端
make
sudo cp ./led.ko .../rootfs/lib/modules/4.1.15/
# 串口
cd /lib/modules/4.1.15/
modprobe led.ko # 右下角红灯亮起
rmmod led.ko # 右下角红灯熄灭
3、改进代码
3.1 修改led.c
完善了字符设备操作函数write,现在通过write来控制LED亮灭,而不是加载卸载了
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include <linux/io.h>
#define LED_MAJOR 200
#define LED_NAME "led"
/* 寄存器物理地址 */
#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;
/* LED状态 */
#define LEDOFF 0
#define LEDON 1
/* LED状态翻转 */
static void led_swtich(u8 sta){
u32 val = 0;
if(sta == LEDOFF){
/* 关灯 */
val = readl(GPIO1_DR); // 32bit读取函数
val |= (1 << 3); // 将bit3置为高电平,关闭LED
writel(val, GPIO1_DR);
}
else if(sta == LEDON){
/* 开灯 */
val = readl(GPIO1_DR); // 32bit读取函数
val &= ~(1 << 3); // 将bit3置为低电平,打开LED
writel(val, GPIO1_DR);
}
}
static int led_open(struct inode * inode, struct file *filp){
return 0;
}
static int led_release(struct inode * inode, struct file *filp){
return 0;
}
static ssize_t led_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos){
int retvalue;
unsigned char databuf[1];
retvalue = copy_from_user(databuf, buf, count);
if(retvalue < 0){
printk("kernel write fauled!\r\n");
return -EFAULT;
}
led_swtich(databuf[0]);
return 0;
}
/* 字符设备操作集合 */
static const struct file_operations led_fope = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write
};
/* 入口 */
static int __init led_init(void){
int ret = 0;
int val = 0;
/* 地址映射 */
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);
/* 初始化 */
val = readl(IMX6U_CCM_CCGR1); // 32bit读取函数
val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。 其中3的二进制为11,~为取反操作。
val |= 3 << 26; // 将26、27位置为1
writel(val, IMX6U_CCM_CCGR1); // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。
writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性
val = readl(GPIO1_GDIR); // 32bit读取函数
val |= 1 << 3; // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
writel(val, GPIO1_GDIR);
/* 关灯 */
val = readl(GPIO1_DR); // 32bit读取函数
val |= (1 << 3); // 将bit3置为高电平,关闭LED
writel(val, GPIO1_DR);
/* 注册设备 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fope);
if(ret<0){
printk("register chrdev failed!\r\n");
return -EIO;
}
printk("led init\r\n");
return 0;
}
/* 出口 */
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);
/* 注销设备 */
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led exit\r\n");
}
/* 驱动加载卸载*/
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
3.2 修改ledAPP.c
led.c中的file_operations结构体注册了.write = led_write,因此在应用程序执行到write(fd, databuf, sizeof(databuf))这一行时会调用内核态中的led_write函数,进行switch
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功; else失败
* 调用 ./ledAPP /dev/led <0:1> 0关灯,1开灯
*/
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[]){
if(argc != 3){ // 判断用法是否错误
printf("Error Usage!\r\n");
return -1;
}
char *filename;
int fd = 0;
unsigned char databuf[1];
int retvalue = 0;
filename = argv[1];
fd = open(filename, O_RDWR); // 读写模式打开驱动文件filename
if(fd <0){
printf("file %s open failed!\r\n");
return -1;
}
databuf[0] = atoi(argv[2]); // char 2 int
retvalue = write(fd, databuf, sizeof(databuf)); // 缓冲区数据写入fd
if(retvalue <0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
close(fd);
return 0;
}
编译复制挂载执行:
# VSCODE终端
make
arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP
sudo cp ledAPP led.ko /.../rootfs/lib/modules/4.1.15/
# 串口
cd /lib/modules/4.1.15
modprobe led.ko # 加载
mknod /dev/led c 200 0 # 创建设备节点文件
./ledAPP /dev/led 1 # 开灯
./ledAPP /dev/led 0 # 关灯
rmmod led.ko # 卸载
其他问题
1. uboot突然不能通过tftp下载zImage
第4.2讲 Linux LED灯驱动实验后半部分讲了一下这个问题

- 检查ubuntu的ip地址是否有变化
- 检查ubuntu能否上网
- 检查开发板和ubuntu能否ping通
- 查看防火墙是否都关闭了
- 检查当前网段中是否有ip重复的问题。先关闭开发板,然后看ubuntu能否ping通开发板ip;然后关闭ubuntu,可以直接用win的终端看能否ping通ubuntu的ip。如果能ping通则有问题,修改重复的ip地址和bootargs、serverip即可。注意1是网关,255是广播,不要用。(开发板直连电脑应该是遇不到这个问题的,开发板接路由器可能出现这个问题)
2. LED闪烁,挂载LED驱动没有作用
如果uboot和内核直接用的是正点原子资料包里(01、例程源码\03、正点原子Uboot和Linux出厂源码)提供的文件,则LED已经被配置为心跳灯,此时自己写的LED驱动会被干扰。
(10、用户手册\01【正点原子】I.MX6U用户快速体验V2.7)3.1. LED 与蜂鸣器测试部分,使用以下命令来关闭心跳灯:
echo none > /sys/class/leds/sys-led/trigger // 改变LED的触发模式

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)