首页 > 其他分享 >你还在开发传统单片机?让单片机用上字符设备驱动!

你还在开发传统单片机?让单片机用上字符设备驱动!

时间:2024-09-28 16:45:12浏览次数:8  
标签:字符 include -- dev 单片机 int gpio 驱动 i2c

本文章为作者原创,未经允许严禁转载。

在刚开始学习单片机的时候,我就想过,当驱动、功能越来越多了应该怎么管理。不同的设备需要不同的函数进行操作,在刚开始我还不太会设计软件架构,当设备功能的数量达到数十个时,代码维护难度就达到了灾难级别。在读大二后,我开始使用freertos并搭配stm32使用。我尝试过将驱动分三级,第一层负责处理与芯片资源sdk的交互、第二层负责数据的传输/设备的控制、第三层负责设备的逻辑处理并提供接口函数。这么做确实让可移植性和可维护性大幅度增加,进行平台的迁移时我只需要修改第一层。但是这种方式还是很死板,因为这些驱动本身依然是硬编码,而且接口非常多样,在大型项目中并发访问、设备管理等方面依更是难以处理。那么有没有什么比较完美的解决方案吗?

linux给出了解决方案:一切设备皆文件。将设备作为文件,使用标准的read write ioctrl进行交互。这很好地解决了裸机开发的痛点,那么有什么办法将它引入单片机吗? 其实当前许多开发框架都提供了类似的支持,包括rtthreads、espidf等。我们不需要从虚拟文件系统开始进行开发。比如espidf就提供了 esp_vfs_t 结构体,它对应linu内核文件<linux/fs.h>中提供的file_operations_t结构体两者使用方式基本相同:
static esp_vfs_t i2c_vfs_node = {
.open = &i2c_dev_open,
.close = &i2c_dev_release,
.write = &i2c_dev_write,
.read = &i2c_dev_read,
.ioctl = &i2c_dev_ioctl,
};
通过这种方式向结构体内传入对应操作的函数指针。
而esp_vfs_register()函数则对应linu内核文件<linux/fs.h>中提供的register_chrdev()函数,可以使用 esp_err_t ret = esp_vfs_register("path", i2c_vfs_node , NULL);的方式传入对应的结构体以及挂载路径,于是,这个驱动就挂载到了path路径下了。之后只需要使用c语言标注的文件操作函数。比如使用:
char *led_dev_name = "/dev/led-1";
int led_file = open(led_dev_name, O_RDWR);
系统就会自动调用传入结构体的open函数。之后的write(led_file,"1",1); close(led_file);也是同理。和linux是完全一致的。此外rtthreads等rtos也提供了许多类似的方式。

但是,这一系列操作非常有espidf的框架特性。用上它,你的代码就和espidf绑定在一起,可移植性就基本得和非espidf平台的设备说再见了。
了么有什么办法让它能够在espidf上愉快地使用字符设备框架并实现尽可能高的可移植性呢?那么就要拿出兼容层大法了。

我为它建造了一个linux内核驱动接口兼容层,让它尽可能贴近linux的开发风格。此外为了灵活性,我也需要实现设备号的分配,所以也另外写了一个设备号分配算法实现了自动分配设备号,并最终作为上下文指针传入esp_vfs_register,此外所有驱动都会被自动添加到更目录下的/dev虚拟路径中。并使用和linux内核尽可能贴近的api以及目录结构。
由于espidf无法像访问文件夹一样打开虚拟目录,也无法这样检索设备。所以我又为它加入了注册表机制,它完全自动执行。
既然实现了这些,那么不加入linux风格的终端也是没有灵魂的,所以必须支持console和shell指令
......

那么,是时候用它编写自己的字符设备驱动了。 使用gpio子系统简简单单地在esp32中点个led灯试试吧!

点击查看代码

#include <linux/gpio.h>   
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/uaccess.h>
#include <linux/bits.h>
#include <linux/types.h>                                               //这些头文件实际上只是简单的兼容层,提供和linux内核接口一致的api。和linux本身无关
#include <linux/printk.h>

#define GPIO_DRIVER_NAME "gpio-1"                                     
#define GPIO_DRIVER_MAJOR 244                                         
#define GPIO_Pin_NUM      2       
static char *gpio_device_name = GPIO_DRIVER_NAME;                       //设备名 
static __u16 gpio_major         = GPIO_DRIVER_MAJOR;                    //主设备号(16bit)
static __u32 device_number = 0;                                         //32位的设备号 
static char buffer;                                                     //缓冲区    
static uint8_t lock = 1;                                                //简单地作为信号量

static int gpio_open(const char *path, int flags, int mode) {
    if(lock == 0)return -EBUSY;
    else{
        lock = 0;
        gpio_request(GPIO_Pin_NUM, gpio_device_name,device_number); //向gpio子系统申请一个gpio引脚,传入了设备名和设备号
    }
    return 0;
}
static int gpio_release(const char *path, int flags, int mode) {
    if(lock == 1)    return -EBUSY;
    else{
        lock = 1;
        gpio_free(GPIO_Pin_NUM);
    }
    return 0;
}
static int gpio_write(int fd, void *buf, size_t count) {    
    if(count < 1)return -EINVAL;
    copy_from_user(&buffer, buf, 1);                                //这只是模仿linux的接口,实际上只是普通拷贝没有用户空间和内核空间的转换
    if(buffer == '1'){
        printk("gpio-1","set gpio pin high\n");
        __gpio_set_value(GPIO_Pin_NUM, 1);                          //通过调用gpio子系统的接口函数设置gpio引脚电平
    }
    else if(buffer == '0'){
        printk("gpio-1","set gpio pin low\n");
        __gpio_set_value(GPIO_Pin_NUM, 0);                          //通过调用gpio子系统的接口函数设置gpio引脚电平
    }
    else{
        return -EINVAL;
    }
    return 1;
}
static int gpio_read(int fd, void *buf, size_t count) {
    return -EINVAL;
}
static file_operations_t gpio_fops = {                               //将操作函数传入到文件操作结构体中                     
    .open  = &gpio_open,
    .read  = &gpio_read,
    .write = &gpio_write,
    .close = &gpio_release,
};
int __init gpio_dev_init(void) {                                     //初始化函数,需要在模块初始化函数中调用 
    device_number = register_chrdev(gpio_major, gpio_device_name, &gpio_fops); //注册设备号和设备名
    if((device_number & 0x0000ffff)==0) {                             //如果次设备号为0则说明注册失败,没有分配到设备号   
        return -EBUSY;
    }
    return 0;
}
void __exit gpio_dev_exit(void) {                                    //退出函数,需要在模块退出函数中调用
    unregister_chrdev(gpio_major, gpio_device_name);
}

然后,只需要调用init函数,它就会被挂载到/dev/gpio-1目录下。

现在我们在main中加入#include "init.h",这个头文件会使用__attribute__((constructor))的GCC编译指令在main函数使用前自动初始化内核。然后将int __init gpio_dev_init(void);驱动初始化程序添加到Add_char_device_driver.c中,然后烧录
ESP32,启动!

之后使用终端查看/dev目录。可以看到里面有许多驱动。

使用echo "1" > /dev/gpio-1 将字符串"1"重定向写入/dev/gpio-1.

根据返回的日志可以知道gpio成功设置成了高电平

点击查看代码

I (817) spi_flash: detected chip: boya
I (820) spi_flash: flash io: qio
W (824) i2c: This driver is an old driver, please migrate your application code to adapt `driver/i2c_master.h`
I (840) [DEFAULT_MOUNT_COMPONENT]:: mount p_0 on /etc
I (844) [DEFAULT_MOUNT_COMPONENT]:: mount p_2 on /home
I (856) [FILE SYSTEM]:: driver installed:dev_path:/dev/SSD1306-1, device_num: 0xcf0001
I (867) [FILE SYSTEM]:: driver installed:dev_path:/dev/gpio-1, device_num: 0xf40001
I (887) [FILE SYSTEM]:: driver installed:dev_path:/dev/i2c-1, device_num: 0xe50001
I (967) [I2C_1_DEV]: USE_IO SCL:5,SDA:4
I (967) [I2C_1_DEV]: Speed: 100000
I (967) [FILE SYSTEM]:: driver installed:dev_path:/dev/i2c-2, device_num: 0xe50002
I (987) [I2C_2_DEV]: USE_IO SCL:18,SDA:19
I (987) [I2C_2_DEV]: Speed: 100000
I (988) gpio: GPIO[0]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 
I (994) [FILE SYSTEM]:: driver installed:dev_path:/dev/button-1, device_num: 0x100f0001
I (1058) BUTTON0_DRIVER: USE pin:0
I (1058) LED_DRIVER: DEVICE_DRIVER LED device init
I (1059) gpio: GPIO[2]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
I (1066) [FILE SYSTEM]:: driver installed:dev_path:/dev/led-1, device_num: 0x100d0001
I (1128) LED_DRIVER: Led use pin: 2
I (1128) sleep: Configure to isolate all GPIO pins in sleep state
I (1129) sleep: Enable automatic switching of GPIO sleep configuration
I (1135) main_task: Started on CPU0
I (1139) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1148) main_task: Calling app_main()
I (1152) main_task: Returned from app_main()

       __
       \  \    ---------
        \  \
         \  \_________   -----
          \ X _________\    ----
          /  /
        /  /       ---------
      /  /    ----------
     ---                           @Sirius  OS 0.04 ES
                                       09/09/2024


root@Sirius OS:/etc ~$: ls
 registry                    shell                      
root@Sirius OS:/etc ~$: cd ..
root@Sirius OS:/ ~$: ls -l
dev            <mount_point>       vfs       
etc            <mount_point>       littlefs  
home           <mount_point>       littlefs  
root@Sirius OS:/ ~$: ls /dev -l
SSD1306-1      <dev>     -13565953       -
gpio-1         <dev>     -15990785       -
i2c-1          <dev>     -15007745       -
i2c-2          <dev>     -15007746       -
button-1       <dev>     -269418497      -
led-1          <dev>     -269287425      -
root@Sirius OS:/ ~$: echo "1" > /dev/gpio-1
I (58137) gpio-1: set gpio pin high

root@Sirius OS:/ ~$: 

那么要怎么使用应用程序调用驱动呢?我演示如何调用i2c-1设备:
我写了一个控制台应用程序,它会使用和linux上完全相同的方式操作i2c设备。它使用
char* path = "/dev/i2c-1";
int fp = open(path, O_RDWR);
打开了/dev/i2c-1设备,然后进入一个循环,使用
ioctl(fp, I2C_SLAVE, (addr << 1));
修改目的设备地址,并对这个地址写入"1"来探测i2c设备。
由于实现了相同api,这段代码你完全可以原封不动地丢到树莓派一类linux开发板上运行。

点击查看代码
#include <stdio.h>
#include <linux/i2c-dev.h>
#include <lwip/sockets.h>
#include <i2cbusses.h>

int do_i2cdetect_cmd(int argc, char **argv)
{
    char* path = "/dev/i2c-1";
    select_i2c_device(path,0x00);
    return 0;
}

int select_i2c_device(char *path,int addr)
{   
    int fp = open(path, O_RDWR);    
    char *data;    
    if (fp < 0) {
        printf("Error: cannot open %s\r\n", path);    
        return -1;
    }
    if(addr == 0x00)
    {
        printf("     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f\r");
        int addr = 0x00;
        int i,j;
        for(int i=0; i<8; i++)
        {
            printf("\r\n %02x: ",i);
            for(int j=0; j<16; j++)
            {
                if(i==0 && j == 0) {j=3;printf("         ");}
                if(i==7 && j == 8) break;
                addr = i*16 + j;
                ioctl(fp, I2C_SLAVE, (addr << 1));
                if(-EIO == write(fp,"1",1))
                {
                    printf("\033[36m");  
                    printf("-- ");
                    printf("\033[37m"); 
                }
                else 
                   printf("%02x ",addr);  
            }
        }
    }
    else
    {
        ioctl(fp, I2C_SLAVE, addr);
         if(-EIO == write(fp, "1", 1))
         printf("Device not found\r\n");
         else 
         printf("Device found at address 0x%02x\r\n", addr);
    }
    printf("\r\n");
    close(fp);
    return 0;
}

之后将它加入Add_my_console_app.c文件中使用 add_console_app("i2cdetect",do_i2cdetect_cmd,"...");注册程序环境变量
我在i2c-1上连接了一块SSD1306 i2c OLED屏幕
然后编译,烧录...启动!

输入i2cdetect,可以看到它成功探测到了OLED屏幕并指出设备地址为3c

点击查看代码
root@Sirius OS:/home/yu/Desktop ~$: i2cdetect
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
 00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
 01: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 02: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 03: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
 04: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 05: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 06: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 07: -- -- -- -- -- -- -- -- 

此外还有很多其它功能...
我将这款框架命名为 Sirius OS
目前它还有一些bug还有一些尚未完成的功能。
所以我暂时还不会发布它
这是最终项目的目录结构:

├── APPS
│ ├── include
│ └── ...
├── Drivers
│ ├── include
│ └── ...
└── kernel
├── arch
│ └── xtensa
│ └── esp32_S3
│ ├── drivers
│ │ └── ...
│ └── include
├── drivers //设备子系统
│ ├── gpio
│ │ └── *.c
│ └── video
│ │ └── src
│ │ └── *.c
│ └── ...
├── fs //文件系统
│ ├── Fatfs
│ │ └── *.c
│ └── *.c
├── init //初始化
│ └── *.c
├── mm
│ └── *.c
├── toos //工具
│ ├── arch
│ │ └── espidf
│ │ └── ...
│ ├──include
│ ├──shell
│ └── ...
└── include
├── asm
│ └── *.h
├── linux
│ └── *.h
└── uapi
└── *.h

内存占用还是有点偏大(我使用ESP32S3N16R8进行开发)
root@Sirius OS:/etc ~$: uname
System Name:
Lunar OS 0.03
ESpidf version:
v5.2.1-dirty
chip message:
esp32s3
2 CPU core(s)
WiFi/BLE, 802.15.4 (Zigbee/Thread)
core speed:
240 MHz
silicon revision
v0.2,
Minimum free heap size:
8470 kb

感谢各位的观看。

标签:字符,include,--,dev,单片机,int,gpio,驱动,i2c
From: https://www.cnblogs.com/Sirius-SaNn/p/18438128

相关文章

  • APP逆向实战:喜马拉雅(OLLVM混淆,字符串加密)
    喜马拉雅抓包:POST/mobile/login/pwd/v3HTTP/1.1Cookie:1&_device=android&fcecf4c4-5ddc-30e3-86b1-6e675f92bfd0&6.6.99;channel=and-f5;impl=com.ximalaya.ting.android;osversion=29;fp=009527657x2022q22564v0500000000000000000000000000000000000000;devic......
  • rust-BufReader逐字符读取
    BufReader有一个fill_buf的方法:fnfill_buf(&mutself)->Result<&[u8]>它可以返回它的内部buffer,如果buffer是空的,就填入更多数据再返回。这样我们就可以逐个读取其内部buffer的字符,且不需要额外申请空间了。通过fill_buf返回的buffer处理完了一些数据之后,可以通过consume来......
  • sha256sum文件哈希值和直接哈希字符串的哈希值不一样
    例如在文件test.txt里写入test没有换行。然后sha256sumtest.txt出来的结果是f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2test.txt但是在这个网站上http://encode.chahuo.com/输入test,然后以sha256方式哈希得到的结果是9f86d081884c7d659a2......
  • Go五种字符串的拼接
    +funcmain(){s1:="hello"s2:="word"s3:=s1+s2fmt.Print(s3)//s3="helloword"}sprintfs1:="hello"s2:="word"s3:=fmt.Sprintf("%s%s",s1,s2)//s3="hell......
  • 单片机中断优先级的解决办法
    为什么会有中断优先级呢?首先如果在INT0按键按下时   T0也刚好记满溢出  机器会先INT0CPU:先响应INT0,响应结束去响应T0 IP寄存器PT0=1保持定时的精度高优先级按自然优先级排序低优先级也按自然优先级排序中断的嵌套    ......
  • 要求实现一个函数 DoubleToStr(double a,int b,char * str),将参数 a 转化为字符串 str
    sprintf函数:sprintf(str,"%.*f",b,a);:sprintf是一个格式化输出函数,类似于printf,但它将输出写入到字符串中而不是标准输出。"%.*f":#include<stdio.h>//将双精度浮点数a转换为字符串str,小数点后保留b位voidDoubleToStr(doublea,intb,char*str){  //......
  • 【C语言标准库函数】标准输入输出函数详解2:字符串输入输出
    目录一、字符串输入函数1.1.gets函数(已废弃)1.1.1.函数简介1.1.2.注意和废弃原因1.2.fgets函数1.2.1.函数简介1.2.2.使用场景1.2.3.注意事项1.2.4.示例二、字符串输出函数2.1.puts函数2.1.1.函数简介2.1.2. 使用场景2.1.3.注意事项2.1.4.示例2.2.......
  • C# 字符串(String)的应用说明一
    一.字符串(String)的应用说明:在C#中,更常见的做法是使用 string 关键字来声明一个字符串变量,也可以使用字符数组来表示字符串。string关键字是 System.String 类的别名。 二.创建String对象的方法说明:1.通过给String 关键字定义一个字符串;2.通过使用String类构......
  • 字符串内置方法二
     isdigit 判断是否是数字 返回bool值strip:去除字符串两端的空格或者换行符应用情况举例:比如输入名字,当输入的信息前后都有空格的时候,需要要strip来清除前后空格,保留主要信息,同时重新对user赋值,释放不必要的存储  split数据分割,返回值是一个列表list  join拼......
  • 字符串内置方法一
    数据类型的内置方法:数据类型对象.方法名(参数)upper 转大写  lower转小写例:s1=HELLO WORLDs2=HELLO YUANstartswith 是否以xxx开头endswith 是否以xxx结尾......