什年代了还在开发传统单片机?让单片机用上字符设备驱动!
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>
感谢各位的观看。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。