点灯大师--IMX.6ULL阿尔法开发板点灯的步骤和五种方式(上)

简介: 点灯大师--IMX.6ULL阿尔法开发板点灯的步骤和五种方式

正点原子阿尔法开发板点灯

1、使能 GPIO1 时钟

GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制,将这两个位都设置位 11 即可。本教程所有例程已经将 I.MX6U 的所有外设时钟都已经打开了,因此这一步可以不用做。

2、设置 GPIO1_IO03 的复用功能

找到 GPIO1_IO03 的复用寄存器“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03”的地址为0X020E0068,然后设置此寄存器,将 GPIO1_IO03 这个 IO 复用为 GPIO 功能,也就是 ALT5。

3、配置 GPIO1_IO03

找到 GPIO1_IO03 的配置寄存器“IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03”的地址为0X020E02F4,根据实际使用情况,配置此寄存器。

4、设置 GPIO

我们已经将 GPIO1_IO03 复用为了 GPIO 功能,所以我们需要配置 GPIO。找到 GPIO3 对应的 GPIO 组寄存器地址,在《IMX6ULL 参考手册》的 1357 页,如图 8.3.1 所示:

5、控制 GPIO 的输出电平

经过前面几步, GPIO1_IO03 已经配置好了,只需要向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1 可控制 GPIO1_IO03 输出高电平,关闭 LED。

五种点灯的方法

1.在一个驱动文件中实现寄存器初始化和寄存器操作

led.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR       200     /* 主设备号 */
#define LED_NAME        "led"   /* 设备名字 */
#define LEDOFF  0               /* 关灯 */
#define LEDON   1               /* 开灯 */
/* 寄存器物理地址 */
#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;
/*
 * @description     : LED打开/关闭
 * @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return          : 无
 */
void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);   
        writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val|= (1 << 3); 
        writel(val, GPIO1_DR);
    }   
}
/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}
/*
 * @description     : 从设备读取数据 
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - buf     : 返回给用户空间的数据缓冲区
 * @param - cnt     : 要读取的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}
/*
 * @description     : 向设备写数据 
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
    ledstat = databuf[0];       /* 获取状态值 */
    if(ledstat == LEDON) {  
        led_switch(LEDON);      /* 打开LED灯 */
    } else if(ledstat == LEDOFF) {
        led_switch(LEDOFF); /* 关闭LED灯 */
    }
    return 0;
}
/*
 * @description     : 关闭/释放设备
 * @param - filp    : 要关闭的设备文件(文件描述符)
 * @return          : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release =  led_release,
};
/*
 * @description : 驱动出口函数
 * @param       : 无
 * @return      : 无
 */
static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;
    /* 初始化LED */
    /* 1、寄存器地址映射 */
    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);
    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);  /* 清楚以前的设置 */
    val |= (3 << 26);   /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);
    /* 3、设置GPIO1_IO03的复用功能,将其复用为
     *    GPIO1_IO03,最后设置IO属性。
     */
    writel(5, SW_MUX_GPIO1_IO03);
    /*寄存器SW_PAD_GPIO1_IO03设置IO属性
     *bit 16:0 HYS关闭
     *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
    writel(0x10B0, SW_PAD_GPIO1_IO03);
    /* 4、设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);   /* 清除以前的设置 */
    val |= (1 << 3);    /* 设置为输出 */
    writel(val, GPIO1_GDIR);
    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);    
    writel(val, GPIO1_DR);
    /* 6、注册字符设备驱动 */
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if(retvalue < 0){
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    return 0;
}
/*
 * @description : 驱动出口函数
 * @param       : 无
 * @return      : 无
 */
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);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

2.使用设备树描述硬件信息

在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-alientek-emmc.dts 文件,在根节点“/”最后面输入如下所示内容:

alphaled {
     #address-cells = <1>;
      #size-cells = <1>;
       compatible = "atkalpha-led";
        status = "okay";
         reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
          0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
          0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
          0X0209C000 0X04 /* GPIO1_DR_BASE */
          0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};

led.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define DTSLED_CNT          1           /* 设备号个数 */
#define DTSLED_NAME         "dtsled"    /* 名字 */
#define LEDOFF                  0           /* 关灯 */
#define LEDON                   1           /* 开灯 */
/* 映射后的寄存器虚拟地址指针 */
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;
/* dtsled设备结构体 */
struct dtsled_dev{
    dev_t devid;            /* 设备号   */
    struct cdev cdev;       /* cdev     */
    struct class *class;        /* 类        */
    struct device *device;  /* 设备    */
    int major;              /* 主设备号   */
    int minor;              /* 次设备号   */
    struct device_node  *nd; /* 设备节点 */
};
struct dtsled_dev dtsled;   /* led设备 */
/*
 * @description     : LED打开/关闭
 * @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return          : 无
 */
void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);   
        writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val|= (1 << 3); 
        writel(val, GPIO1_DR);
    }   
}
/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &dtsled; /* 设置私有数据 */
    return 0;
}
/*
 * @description     : 从设备读取数据 
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - buf     : 返回给用户空间的数据缓冲区
 * @param - cnt     : 要读取的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}
/*
 * @description     : 向设备写数据 
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
    ledstat = databuf[0];       /* 获取状态值 */
    if(ledstat == LEDON) {  
        led_switch(LEDON);      /* 打开LED灯 */
    } else if(ledstat == LEDOFF) {
        led_switch(LEDOFF); /* 关闭LED灯 */
    }
    return 0;
}
/*
 * @description     : 关闭/释放设备
 * @param - filp    : 要关闭的设备文件(文件描述符)
 * @return          : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}
/* 设备操作函数 */
static struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release =  led_release,
};
/*
 * @description : 驱动出口函数
 * @param       : 无
 * @return      : 无
 */
static int __init led_init(void)
{
    u32 val = 0;
    int ret;
    u32 regdata[14];
    const char *str;
    struct property *proper;
    /* 获取设备树中的属性数据 */
    /* 1、获取设备节点:alphaled */
    dtsled.nd = of_find_node_by_path("/alphaled");
    if(dtsled.nd == NULL) {
        printk("alphaled node nost find!\r\n");
        return -EINVAL;
    } else {
        printk("alphaled node find!\r\n");
    }
    /* 2、获取compatible属性内容 */
    proper = of_find_property(dtsled.nd, "compatible", NULL);
    if(proper == NULL) {
        printk("compatible property find failed\r\n");
    } else {
        printk("compatible = %s\r\n", (char*)proper->value);
    }
    /* 3、获取status属性内容 */
    ret = of_property_read_string(dtsled.nd, "status", &str);
    if(ret < 0){
        printk("status read failed!\r\n");
    } else {
        printk("status = %s\r\n",str);
    }
    /* 4、获取reg属性内容 */
    ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
    if(ret < 0) {
        printk("reg property read failed!\r\n");
    } else {
        u8 i = 0;
        printk("reg data:\r\n");
        for(i = 0; i < 10; i++)
            printk("%#X ", regdata[i]);
        printk("\r\n");
    }
    /* 初始化LED */
#if 0
    /* 1、寄存器地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
    SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
    SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
    GPIO1_DR = ioremap(regdata[6], regdata[7]);
    GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else
    IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
    SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
    SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
    GPIO1_DR = of_iomap(dtsled.nd, 3);
    GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif
    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);  /* 清楚以前的设置 */
    val |= (3 << 26);   /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);
    /* 3、设置GPIO1_IO03的复用功能,将其复用为
     *    GPIO1_IO03,最后设置IO属性。
     */
    writel(5, SW_MUX_GPIO1_IO03);
    /*寄存器SW_PAD_GPIO1_IO03设置IO属性
     *bit 16:0 HYS关闭
     *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
    writel(0x10B0, SW_PAD_GPIO1_IO03);
    /* 4、设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);   /* 清除以前的设置 */
    val |= (1 << 3);    /* 设置为输出 */
    writel(val, GPIO1_GDIR);
    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);    
    writel(val, GPIO1_DR);
    /* 注册字符设备驱动 */
    /* 1、创建设备号 */
    if (dtsled.major) {     /*  定义了设备号 */
        dtsled.devid = MKDEV(dtsled.major, 0);
        register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
    } else {                        /* 没有定义设备号 */
        alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */
        dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
        dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
    }
    printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);  
    /* 2、初始化cdev */
    dtsled.cdev.owner = THIS_MODULE;
    cdev_init(&dtsled.cdev, &dtsled_fops);
    /* 3、添加一个cdev */
    cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);
    /* 4、创建类 */
    dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
    if (IS_ERR(dtsled.class)) {
        return PTR_ERR(dtsled.class);
    }
    /* 5、创建设备 */
    dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
    if (IS_ERR(dtsled.device)) {
        return PTR_ERR(dtsled.device);
    }
    return 0;
}
/*
 * @description : 驱动出口函数
 * @param       : 无
 * @return      : 无
 */
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);
    /* 注销字符设备驱动 */
    cdev_del(&dtsled.cdev);/*  删除cdev */
    unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */
    device_destroy(dtsled.class, dtsled.devid);
    class_destroy(dtsled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

3.pinctl和gpio子系统描述硬件信息

3.1修改设备树文件

添加 pinctrl 节点

I.MX6U-ALPHA 开发板上的 LED 灯使用了 GPIO1_IO03 这个 PIN,打开 imx6ull-alientekemmc.dts,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:

pinctrl_led: ledgrp {
     fsl,pins = <
      MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
       >;
};

第 3 行将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0

添加 LED 设备节点

在根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:

gpioled {
     #address-cells = <1>;
      #size-cells = <1>;
       compatible = "atkalpha-gpioled";
        pinctrl-names = "default";
         pinctrl-0 = <&pinctrl_led>;
          led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
           status = "okay";
};

第 6 行, pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点。

第 7 行, led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号。

3、检查 PIN 是否被其他外设使用

3.2驱动代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define GPIOLED_CNT         1           /* 设备号个数 */
#define GPIOLED_NAME        "gpioled"   /* 名字 */
#define LEDOFF              0           /* 关灯 */
#define LEDON               1           /* 开灯 */
/* gpioled设备结构体 */
struct gpioled_dev{
    dev_t devid;            /* 设备号   */
    struct cdev cdev;       /* cdev     */
    struct class *class;    /* 类        */
    struct device *device;  /* 设备    */
    int major;              /* 主设备号   */
    int minor;              /* 次设备号   */
    struct device_node  *nd; /* 设备节点 */
    int led_gpio;           /* led所使用的GPIO编号        */
};
struct gpioled_dev gpioled; /* led设备 */
/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpioled; /* 设置私有数据 */
    return 0;
}
/*
 * @description     : 从设备读取数据 
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - buf     : 返回给用户空间的数据缓冲区
 * @param - cnt     : 要读取的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}
/*
 * @description     : 向设备写数据 
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    struct gpioled_dev *dev = filp->private_data;
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
    ledstat = databuf[0];       /* 获取状态值 */
    if(ledstat == LEDON) {  
        gpio_set_value(dev->led_gpio, 0);   /* 打开LED灯 */
    } else if(ledstat == LEDOFF) {
        gpio_set_value(dev->led_gpio, 1);   /* 关闭LED灯 */
    }
    return 0;
}
/*
 * @description     : 关闭/释放设备
 * @param - filp    : 要关闭的设备文件(文件描述符)
 * @return          : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release =  led_release,
};
/*
 * @description : 驱动出口函数
 * @param       : 无
 * @return      : 无
 */
static int __init led_init(void)
{
    int ret = 0;
    /* 设置LED所使用的GPIO */
    /* 1、获取设备节点:gpioled */
    gpioled.nd = of_find_node_by_path("/gpioled");
    if(gpioled.nd == NULL) {
        printk("gpioled node not find!\r\n");
        return -EINVAL;
    } else {
        printk("gpioled node find!\r\n");
    }
    /* 2、 获取设备树中的gpio属性,得到LED所使用的LED编号 */
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
    if(gpioled.led_gpio < 0) {
        printk("can't get led-gpio");
        return -EINVAL;
    }
    printk("led-gpio num = %d\r\n", gpioled.led_gpio);
    /* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if(ret < 0) {
        printk("can't set gpio!\r\n");
    }
    /* 注册字符设备驱动 */
    /* 1、创建设备号 */
    if (gpioled.major) {        /*  定义了设备号 */
        gpioled.devid = MKDEV(gpioled.major, 0);
        register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
    } else {                        /* 没有定义设备号 */
        alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);  /* 申请设备号 */
        gpioled.major = MAJOR(gpioled.devid);   /* 获取分配号的主设备号 */
        gpioled.minor = MINOR(gpioled.devid);   /* 获取分配号的次设备号 */
    }
    printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);   
    /* 2、初始化cdev */
    gpioled.cdev.owner = THIS_MODULE;
    cdev_init(&gpioled.cdev, &gpioled_fops);
    /* 3、添加一个cdev */
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
    /* 4、创建类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if (IS_ERR(gpioled.class)) {
        return PTR_ERR(gpioled.class);
    }
    /* 5、创建设备 */
    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if (IS_ERR(gpioled.device)) {
        return PTR_ERR(gpioled.device);
    }
    return 0;
}
/*
 * @description : 驱动出口函数
 * @param       : 无
 * @return      : 无
 */
static void __exit led_exit(void)
{
    /* 注销字符设备驱动 */
    cdev_del(&gpioled.cdev);/*  删除cdev */
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); /* 注销设备号 */
    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

4.platform驱动

driver.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LEDDEV_CNT      1           /* 设备号长度    */
#define LEDDEV_NAME     "platled"   /* 设备名字     */
#define LEDOFF          0
#define LEDON           1
/* 寄存器名 */
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;
/* leddev设备结构体 */
struct leddev_dev{
    dev_t devid;            /* 设备号  */
    struct cdev cdev;       /* cdev     */
    struct class *class;    /* 类        */
    struct device *device;  /* 设备       */
    int major;              /* 主设备号 */      
};
struct leddev_dev leddev;   /* led设备 */
/*
 * @description     : LED打开/关闭
 * @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return          : 无
 */
void led0_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON){
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);   
        writel(val, GPIO1_DR);
    }else if(sta == LEDOFF){
        val = readl(GPIO1_DR);
        val|= (1 << 3); 
        writel(val, GPIO1_DR);
    }   
}
/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &leddev; /* 设置私有数据  */
    return 0;
}
/*
 * @description     : 向设备写数据 
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        return -EFAULT;
    }
    ledstat = databuf[0];       /* 获取状态值 */
    if(ledstat == LEDON) {
        led0_switch(LEDON);     /* 打开LED灯 */
    }else if(ledstat == LEDOFF) {
        led0_switch(LEDOFF);    /* 关闭LED灯 */
    }
    return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
};
/*
 * @description     : flatform驱动的probe函数,当驱动与
 *                    设备匹配以后此函数就会执行
 * @param - dev     : platform设备
 * @return          : 0,成功;其他负值,失败
 */
static int led_probe(struct platform_device *dev)
{   
    int i = 0;
    int ressize[5];
    u32 val = 0;
    struct resource *ledsource[5];
    printk("led driver and device has matched!\r\n");
    /* 1、获取资源 */
    for (i = 0; i < 5; i++) {
        ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i); /* 依次MEM类型资源 */
        if (!ledsource[i]) {
            dev_err(&dev->dev, "No MEM resource for always on\n");
            return -ENXIO;
        }
        ressize[i] = resource_size(ledsource[i]);   
    }   
    /* 2、初始化LED */
    /* 寄存器地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]);
    SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, ressize[1]);
    SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, ressize[2]);
    GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]);
    GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]);
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);              /* 清除以前的设置 */
    val |= (3 << 26);               /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);
    /* 设置GPIO1_IO03复用功能,将其复用为GPIO1_IO03 */
    writel(5, SW_MUX_GPIO1_IO03);
    writel(0x10B0, SW_PAD_GPIO1_IO03);
    /* 设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);           /* 清除以前的设置 */
    val |= (1 << 3);            /* 设置为输出 */
    writel(val, GPIO1_GDIR);
    /* 默认关闭LED1 */
    val = readl(GPIO1_DR);
    val |= (1 << 3) ;   
    writel(val, GPIO1_DR);
    /* 注册字符设备驱动 */
    /*1、创建设备号 */
    if (leddev.major) {     /*  定义了设备号 */
        leddev.devid = MKDEV(leddev.major, 0);
        register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
    } else {                        /* 没有定义设备号 */
        alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME); /* 申请设备号 */
        leddev.major = MAJOR(leddev.devid); /* 获取分配号的主设备号 */
    }
    /* 2、初始化cdev */
    leddev.cdev.owner = THIS_MODULE;
    cdev_init(&leddev.cdev, &led_fops);
    /* 3、添加一个cdev */
    cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
    /* 4、创建类 */
    leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
    if (IS_ERR(leddev.class)) {
        return PTR_ERR(leddev.class);
    }
    /* 5、创建设备 */
    leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
    if (IS_ERR(leddev.device)) {
        return PTR_ERR(leddev.device);
    }
    return 0;
}
/*
 * @description     : platform驱动的remove函数,移除platform驱动的时候此函数会执行
 * @param - dev     : platform设备
 * @return          : 0,成功;其他负值,失败
 */
static int led_remove(struct platform_device *dev)
{
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    cdev_del(&leddev.cdev);/*  删除cdev */
    unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
    device_destroy(leddev.class, leddev.devid);
    class_destroy(leddev.class);
    return 0;
}
/* platform驱动结构体 */
static struct platform_driver led_driver = {
    .driver     = {
        .name   = "imx6ul-led",         /* 驱动名字,用于和设备匹配 */
    },
    .probe      = led_probe,
    .remove     = led_remove,
};
/*
 * @description : 驱动模块加载函数
 * @param       : 无
 * @return      : 无
 */
static int __init leddriver_init(void)
{
    return platform_driver_register(&led_driver);
}
/*
 * @description : 驱动模块卸载函数
 * @param       : 无
 * @return      : 无
 */
static void __exit leddriver_exit(void)
{
    platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");

device.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/* 
 * 寄存器地址定义
 */
#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)
#define REGISTER_LENGTH             4
/* @description     : 释放flatform设备模块的时候此函数会执行   
 * @param - dev     : 要释放的设备 
 * @return          : 无
 */
static void led_release(struct device *dev)
{
    printk("led device released!\r\n"); 
}
/*  
 * 设备资源信息,也就是LED0所使用的所有寄存器
 */
static struct resource led_resources[] = {
    [0] = {
        .start  = CCM_CCGR1_BASE,
        .end    = (CCM_CCGR1_BASE + REGISTER_LENGTH - 1),
        .flags  = IORESOURCE_MEM,
    },  
    [1] = {
        .start  = SW_MUX_GPIO1_IO03_BASE,
        .end    = (SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),
        .flags  = IORESOURCE_MEM,
    },
    [2] = {
        .start  = SW_PAD_GPIO1_IO03_BASE,
        .end    = (SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),
        .flags  = IORESOURCE_MEM,
    },
    [3] = {
        .start  = GPIO1_DR_BASE,
        .end    = (GPIO1_DR_BASE + REGISTER_LENGTH - 1),
        .flags  = IORESOURCE_MEM,
    },
    [4] = {
        .start  = GPIO1_GDIR_BASE,
        .end    = (GPIO1_GDIR_BASE + REGISTER_LENGTH - 1),
        .flags  = IORESOURCE_MEM,
    },
};
/*
 * platform设备结构体 
 */
static struct platform_device leddevice = {
    .name = "imx6ul-led",
    .id = -1,
    .dev = {
        .release = &led_release,
    },
    .num_resources = ARRAY_SIZE(led_resources),
    .resource = led_resources,
};
/*
 * @description : 设备模块加载 
 * @param       : 无
 * @return      : 无
 */
static int __init leddevice_init(void)
{
    return platform_device_register(&leddevice);
}
/*
 * @description : 设备模块注销
 * @param       : 无
 * @return      : 无
 */
static void __exit leddevice_exit(void)
{
    platform_device_unregister(&leddevice);
}
module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");

5.设备树platform

设备树

添加 pinctrl 节点

I.MX6U-ALPHA 开发板上的 LED 灯使用了 GPIO1_IO03 这个 PIN,打开 imx6ull-alientekemmc.dts,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:

pinctrl_led: ledgrp {
     fsl,pins = <
      MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
       >;
};

第 3 行将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0

添加 LED 设备节点

在根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:

gpioled {
     #address-cells = <1>;
      #size-cells = <1>;
       compatible = "atkalpha-gpioled";
        pinctrl-names = "default";
         pinctrl-0 = <&pinctrl_led>;
          led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
           status = "okay";
};

第 6 行, pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点。

第 7 行, led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号。

3、检查 PIN 是否被其他外设使用

驱动代码

driver.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LEDDEV_CNT      1               /* 设备号长度    */
#define LEDDEV_NAME     "dtsplatled"    /* 设备名字     */
#define LEDOFF          0
#define LEDON           1
/* leddev设备结构体 */
struct leddev_dev{
    dev_t devid;                /* 设备号  */
    struct cdev cdev;           /* cdev     */
    struct class *class;        /* 类        */
    struct device *device;      /* 设备       */
    int major;                  /* 主设备号 */  
    struct device_node *node;   /* LED设备节点 */
    int led0;                   /* LED灯GPIO标号 */
};
struct leddev_dev leddev;       /* led设备 */
/*
 * @description     : LED打开/关闭
 * @param - sta     : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return          : 无
 */
void led0_switch(u8 sta)
{
    if (sta == LEDON )
        gpio_set_value(leddev.led0, 0);
    else if (sta == LEDOFF)
        gpio_set_value(leddev.led0, 1); 
}
/*
 * @description     : 打开设备
 * @param - inode   : 传递给驱动的inode
 * @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体。
 * @return          : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &leddev; /* 设置私有数据  */
    return 0;
}
/*
 * @description     : 向设备写数据 
 * @param - filp    : 设备文件,表示打开的文件描述符
 * @param - buf     : 要写给设备写入的数据
 * @param - cnt     : 要写入的数据长度
 * @param - offt    : 相对于文件首地址的偏移
 * @return          : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[2];
    unsigned char ledstat;
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
    ledstat = databuf[0];
    if (ledstat == LEDON) {
        led0_switch(LEDON);
    } else if (ledstat == LEDOFF) {
        led0_switch(LEDOFF);
    }
    return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
};
/*
 * @description     : flatform驱动的probe函数,当驱动与
 *                    设备匹配以后此函数就会执行
 * @param - dev     : platform设备
 * @return          : 0,成功;其他负值,失败
 */
static int led_probe(struct platform_device *dev)
{   
    printk("led driver and device was matched!\r\n");
    /* 1、设置设备号 */
    if (leddev.major) {
        leddev.devid = MKDEV(leddev.major, 0);
        register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
    } else {
        alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
        leddev.major = MAJOR(leddev.devid);
    }
    /* 2、注册设备      */
    cdev_init(&leddev.cdev, &led_fops);
    cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
    /* 3、创建类      */
    leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
    if (IS_ERR(leddev.class)) {
        return PTR_ERR(leddev.class);
    }
    /* 4、创建设备 */
    leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
    if (IS_ERR(leddev.device)) {
        return PTR_ERR(leddev.device);
    }
    /* 5、初始化IO */   
    leddev.node = of_find_node_by_path("/gpioled");
    if (leddev.node == NULL){
        printk("gpioled node nost find!\r\n");
        return -EINVAL;
    } 
    leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
    if (leddev.led0 < 0) {
        printk("can't get led-gpio\r\n");
        return -EINVAL;
    }
    gpio_request(leddev.led0, "led0");
    gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平    */
    return 0;
}
/*
 * @description     : platform驱动的remove函数,移除platform驱动的时候此函数会执行
 * @param - dev     : platform设备
 * @return          : 0,成功;其他负值,失败
 */
static int led_remove(struct platform_device *dev)
{
    gpio_set_value(leddev.led0, 1);     /* 卸载驱动的时候关闭LED */
    gpio_free(leddev.led0);             /* 释放IO             */
    cdev_del(&leddev.cdev);             /*  删除cdev */
    unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
    device_destroy(leddev.class, leddev.devid);
    class_destroy(leddev.class);
    return 0;
}
/* 匹配列表 */
static const struct of_device_id led_of_match[] = {
    { .compatible = "atkalpha-gpioled" },
    { /* Sentinel */ }
};
/* platform驱动结构体 */
static struct platform_driver led_driver = {
    .driver     = {
        .name   = "imx6ul-led",         /* 驱动名字,用于和设备匹配 */
        .of_match_table = led_of_match, /* 设备树匹配表        */
    },
    .probe      = led_probe,
    .remove     = led_remove,
};
/*
 * @description : 驱动模块加载函数
 * @param       : 无
 * @return      : 无
 */
static int __init leddriver_init(void)
{
    return platform_driver_register(&led_driver);
}
/*
 * @description : 驱动模块卸载函数
 * @param       : 无
 * @return      : 无
 */
static void __exit leddriver_exit(void)
{
    platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");

目录
相关文章
|
Ubuntu Linux Windows
Linux开发环境配置详细过程--正点原子阿尔法开发板
Linux开发环境配置详细过程--正点原子阿尔法开发板
464 0
|
4月前
stm32f407探索者开发板(十九)——外部中断实验-EXIT
stm32f407探索者开发板(十九)——外部中断实验-EXIT
296 0
|
4月前
|
芯片
stm32f407探索者开发板(二十)——独立看门狗实验
stm32f407探索者开发板(二十)——独立看门狗实验
300 0
|
6月前
|
传感器 Linux 编译器
不同平台下的点灯代码,你在点灯的哪个段位?
不同平台下的点灯代码,你在点灯的哪个段位?
|
芯片
点灯大师--IMX.6ULL阿尔法开发板点灯的步骤和五种方式(下)
点灯大师--IMX.6ULL阿尔法开发板点灯的步骤和五种方式
141 0
点灯大师--IMX.6ULL阿尔法开发板点灯的步骤和五种方式(下)
|
C语言 Python Windows
MicroPython 玩转硬件系列2:点灯实验
MicroPython 玩转硬件系列2:点灯实验
|
Linux 开发工具
瑞芯微RV1109配置GPIO设备树修改笔记(熟悉新平台从点灯大法开始)
瑞芯微RV1109配置GPIO设备树修改笔记(熟悉新平台从点灯大法开始)
280 0
|
Linux
手把手教你写Linux设备驱动---定时器(一)(基于友善之臂4412开发板)
手把手教你写Linux设备驱动---定时器(一)(基于友善之臂4412开发板)
133 0
正点原子战舰开发板---串口调试(硬件调试的一点经验吧)
正点原子战舰开发板---串口调试(硬件调试的一点经验吧)
249 0
【蓝桥杯嵌入式】PWM的设置,原理图解析与代码实现(第十一届省赛为例)——STM32
【蓝桥杯嵌入式】PWM的设置,原理图解析与代码实现(第十一届省赛为例)——STM32
403 0
【蓝桥杯嵌入式】PWM的设置,原理图解析与代码实现(第十一届省赛为例)——STM32