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

cnblogs 2024-09-30 08:15:00 阅读 75

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

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

linux给出了解决方案:一切设备皆文件。将设备作为文件,使用标准的read write ioctrl进行交互。这很好地解决了裸机开发的痛点,那么有什么办法将它引入单片机吗? 其实当前许多开发框架都提供了类似的支持,包括rtthreads、espidf等。我们不需要从虚拟文件系统开始进行开发。比如espidf就提供了 esp_vfs_t 结构体,它对应linu内核文件<linux/fs.h>中提供的file_operations_t结构体两者使用方式基本相同:

<code>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语言标注的文件操作函数。比如使用:

int led_file = open("/dev/led-1", O_RDWR);

系统就会自动调用传入结构体的open函数。而write(led_file,"1",1); close(led_file);等函数也是同理。和桌面端是基本一致的。

但是,这一系列操作非常有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);

}

![](https://img2024.cnblogs.com/blog/3431065/202409/3431065-20240928185516439-1988094549.png)

然后,只需要调用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成功设置成了高电平。write函数中的日志也被打印

这里中途拿起单片机拍照导致串口断开了一下。

点击查看代码

<code>

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:/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

root@Sirius OS:/ ~$:

内存占用似乎有点偏大(我使用ESP32S3N16R8进行开发)......

字符设备的调用也是和linux上的一样?在这里我写了一个控制台应用程序作为演示,它会使用和linux上完全相同的方式操作i2c设备。

char* path = "/dev/i2c-1";

int fp = open(path, O_RDWR); //打开了/dev/i2c-1设备,然后进入一个循环

ioctl(fp, I2C_SLAVE, (addr << 1)); //在循环中使用ioctl修改目的设备地址,并对这个地址写入"1"来探测i2c设备。

if(-EIO == write(fp,"1",1))

由于实现了相同api,这段代码你完全可以原封不动地丢到树莓派一类linux开发板上运行。你会得到一样的效果

点击查看代码

<code>

#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,"...");注册控制台程序的环境变量

然后编译,烧录...启动!

输入i2cdetect,可以看到它成功探测到了i2c-1上连接的SSD1306 i2c OLED屏幕并指出设备地址为3c

点击查看代码

<code>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

│ │ └── ...

│ └── video

│ │ └── src

│ │ └── ...

│ └── ...

├── fs

│ └── ...

├── init

│ └── ...

├── mm

│ └── ...

├── toos

│ ├── arch

│ │ └── espidf

│ │ └── ...

│ ├──include

│ ├──shell

│ └── ...

└── include

├── asm

│ └── *.h

├── linux

│ └── *.h

└── uapi

└── *.h

</details>

感谢各位的观看。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。