前面已经实现了设备号、字符设备的注册以及节点的自动创建,接下来将实际操作外设,下面将直接通过寄存器操作LED,后续还可以通过设备树来操作。
一般对外设的初始化,只要执行一次即可,所以放在驱动入口函数 xxx_init
在裸机开发时,因为没有OS,所以一般通过直接读写物理地址来操作寄存器;有了OS后,每个进程都会被分配一个虚拟地址空间,使每个进程在自己的地址空间中运行,防止不同进程随意修改或篡改物理地址的内容。
因此,这里需要先通过物理地址拿到对应的虚拟地址,然后再通过操作虚拟地址来驱动外设。建立映射使用 ioremap 函数,函数声明如下:
/** 建立物理地址和虚拟地址的映射
* @param cookie 物理地址
* @param size 物理地址的长度
* @return 虚拟地址
*/
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
extern void __iomem *__arm_ioremap(phys_addr_t, size_t, unsigned int);
具体操作如下:
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE 0x20C406C // 时钟源
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE 0x20E0068 // IO 复用
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE 0x20E02F4 // 复用引脚初始化
#define GPIO1_GDIR_BASE 0x209C004 // 输出方向
#define GPIO1_DR_BASE 0x209C000 // LED输出引脚
/* 寄存器虚拟地址 */
static void __iomem* CCM_CCGR1;
static void __iomem* IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
static void __iomem* IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03;
static void __iomem* GPIO1_GDIR;
static void __iomem* GPIO1_DR;
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
// ...
/* 建立物理地址和虚拟地址的映射 */
CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE, 4);
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
// ...
}
Linux内核提供了虚拟地址的读写函数 readl / writel。函数声明如下:
/** 从起始虚拟地址开始,读取32bit的内容
* @param addr 虚拟地址
* @return 返回读取的32bit的内容
*/
static inline u32 readl(const volatile void __iomem *addr);
/** 向虚拟地址写入内容
* @param value 要写入的内容
* @param addr 虚拟地址
*/
static inline void writel(u32 value, volatile void __iomem *addr);
具体操作如下:
static int __init chrdevbase_init(void)
{
/* 建立物理地址和虚拟地址的映射 */
/* GPIO1 时钟源初始化 */
val = readl(CCM_CCGR1);
val |= (3 << 26);
writel(val, CCM_CCGR1);
/* IO复用 */
writel(5, IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
/* IO复用引脚初始化 */
writel(0x10B0, IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03);
/* GPIO1_IO03 输出使能 */
val = readl(GPIO1_GDIR);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
/* LED 熄灭(GPIO1的第3个引脚为高电平) */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 注册设备号、初始化字符设备 */
}
当上层应用层调用 open 函数打开文件,获取文件描述符时,内核会调用驱动中对应的 open 函数。pfile->private_data 用于在当前内核模块的文件操作函数之间传递数据,相比于直接使用全局变量,有如下好处:
/*
* @description : 打开设备
* @param – pinode : 传递给驱动的 inode
* @param - pfile : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *pinode, struct file *pfile)
{
printk("open chrdevbase\n");
// 后续其他文件操作函数可直接通过pfile获取,而无需通过全局变量
pfile->private_data = &chrdev_led;
return 0;
}
在实现 write 操作时,参数 buf 是用户缓冲区,我们需要先将用户缓冲区的数据拷贝到内核缓冲区,其目的如下:
将用户数据拷贝到内核缓冲区,调用的 API 为 copy_from_user
enum LED_STAT {
LED_ON,
LED_OFF
};
static void led_on(void)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}
static void led_off(void)
{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
/*
* @description : 向设备写数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 要给设备写入的数据(用户缓冲区)
* @param - cnt : 要写入的数据长度
* @param - offset : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *pfile, const char __user *buf, size_t cnt, loff_t *offset)
{
// 获取模块数据
struct chrdev_led_t* pdev = pfile->private_data;
printk("write chrdevbase\n");
unsigned char databuf[1];
unsigned char ledstat;
// 将数据从用户缓冲区拷贝到内核缓冲区
int ret = copy_from_user(databuf, buf, cnt);
if(ret != 0)
return 0;
ledstat = buf[0] - '0';
if (ledstat == LED_ON)
{
led_on();
}
else if(ledstat == LED_OFF)
{
led_off();
}
return cnt;
}
和write 操作同理,如果需要将内核数据拷贝到用户缓冲区,同样需要通过 API 来实现,这里使用的API 为 copy_to_user
/*
* @description : 从设备读取数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 缓冲区长度
* @param - offset : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *pfile, char __user *buf, size_t cnt, loff_t *offset)
{
/* 用户实现具体功能 */
struct chrdev_led_t* pdev = pfile->private_data;
const char* msg = "hello, user";
int ret = copy_to_user(buf, msg, cnt);
if(ret == 0)
{
printk("kernel send data ok!\n");
}
else
{
printk("kernel send data failed!\n");
}
return 0;
}
因为本人使用的板子,在上电时内核初始化了中断,每隔一段时间就会自动进入到中断切换 LED 的状态,所以这里将通过 while 循环让 LED 常亮 / 常灭(一般执行一次就行,这里因为有中断才这么做)
命令的基本格式为: ./
具体测试用例的实现如下:
#include
#include
#include
#include
#include
#include
void printHelp()
{
printf("usage: ./chrdevbaseApp \n");
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
printHelp();
return -1;
}
char* driver_path = argv[1]; // 位置0 保存的是 ./chrdevbaseApp
char* led_stat = argv[2];
int fd = open(driver_path, O_WRONLY);
if (fd < 0)
{
perror("open file failed");
return -2;
}
while (1)
{
write(fd, led_stat, 1);
}
close(fd);
return 0;
}
假设生成的执行文件名为 chrdevbaseApp,自动创建的驱动节点路径 driver_path 为 /dev/chrdevbase
输入命令 ./chrdevbaseApp /dev/chrdevbase 0,LED常亮