开发日记:Linux开发环境下ADC驱动程序编写
本文系统讲解了Linux环境下ADC驱动与应用程序的开发流程,涵盖驱动架构设计、数据采集实现及用户态数据读取方法。该实现方案完整展示了从底层驱动到上层应用的ADC系统开发过程,兼具理论指导和实践参考价值,特别适用于嵌入式数据采集系统的开发场景。
ADC驱动程序编写
一. adc框架架构
adc框架 = dts框架 + paltform框架 + iio框架
dts框架,前文设备树子设备的框架。
platform框架,简化设备与驱动的联系,api接口更隐蔽了。
iio框架,专门用于管理adc或dac驱动程序。
二. adc驱动开发流程
有一个新的编写程序框架概念:从结尾往源头编写驱动程序。下面是编写的一个思路流程:
驱动程序
├─ 驱动入口函数
│ ├─ 注册平台设备驱动
│ └─ 平台设备驱动结构体--adc_driver
├─ 驱动出口函数
│ ├─ 注销平台设备驱动
│ └─ 平台设备驱动结构体--adc_driver
├─ 平台设备驱动结构体--adc_driver
│ ├─ probe
│ ├─ remove
│ └─ driver
│ └─ of_match_table
├─ probe
│ ├─ 注册字符设备驱动
│ └─ 获取iio通道
├─ remove
│ └─ 注销字符设备驱动
├─ of_match_table
├─ file_operations--操作函数集
│ ├─ .owner
│ ├─ .open
│ ├─ .write
│ ├─ .read
│ └─ .adc_dev_ioctl
└─ 编写操作函数
总体驱动开发流程如上所示,其中主要以dts框架下注册一个字符驱动设备为核心,在其中通过platform平台设备开发简化设备树注册流程,再通过iio驱动的api接口–ioctl函数,去读取adc数据。
其中有个新的知识点,iio驱动的api接口,通过接受命令符,使用api接口使用adc通道,最终读取数据发送至用户空间。
/*********************************************************************
* @fn adc_dev_ioctl
*
* @brief iio子系统的api函数,完成指定命令,主要为读取数据
* @param file:读取设备参数 cmd:ioctl操作的命令 arg:用户空间的地址
* @return none
*/
long adc_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int error; //错误判断参数
int scale;
switch (cmd)
{
//情况1
case CMD_READ_SCALE:
error = iio_read_channel_raw(adcdevice.adc_channel, &scale); //iio子系统api函数:获取adc通道数据
//错误判断
if(error != 0)
{
printk("iio read channel error!!! \n");
return -1;
}
error = copy_to_user((int *)arg, &scale, sizeof(scale));
if(error != 0)
{
printk("copy to user error!!! \n");
return -2;
}
break;
//默认情况
default:
break;
}
return 0;
}
App应用程序编写
一. Linux文件流读取
在ADC驱动编写中,需要连续不断的读取传感器数据,该情况下,不能使用cat命令一次次读取数据,需要编写对应的APP软件。同时对比ioctl,文件流读取方式专门对项目中需要读取数据的部分,ioctl为iio通道的api函数,面对少量数据的读取,且有一定的泄露风险(tip:因为通过命令的形式去使得读取数据)。
接下来介绍几个关键的API函数。
1. 打开文件流
打开文件流使用fopen函数,函数原型如下:FILE *fopen(const char *pathname, const char *mode)
函数参数和返回值含义如下:pathname:需要打开的文件流路径。mode:打开方式,可选的打开方式如下图:![![[mode打开方式.png]]](https://i-blog.csdnimg.cn/direct/f2a8b5a3cbc347b0b837ac1f87ef2a24.png)
返回值:NULL,打开错误;其他值,打开成功的文件流指针,为 FILE 类型。
2. 关闭文件流
关闭文件流使用函数 fclose,函数原型如下:
int fclose(FILE *stream)
函数参数和返回值含义如下:
stream:要关闭的文件流指针。
返回值:0,关闭成功;EOF,关闭错误。
3. 读取文件流
要读取文件流使用 fread 函数,函数原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
fread 函数用于从给定的输入流中读取最多 nmemb 个对象到数组 ptr 中,函数参数和返回值含义如下:
ptr:要读取的数组中首个对象的指针。
size:每个对象的大小。
nmemb:要读取的对象个数。
stream:要读取的文件流。
返回值:返回读取成功的对象个数,如果出现错误或到文件末尾,那么返回一个短计数值(或者 0)。
4. 写文件流
要向文件流写入数据,使用 fwrite 函数,函数原型如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite 函数用于向给定的文件流中写入最多 nmemb 个对象,函数参数和返回值含义如下:
ptr:要写入的数组中首个对象的指针。
size:每个对象的大小。
nmemb:要写入的对象个数。
stream:要写入的文件流。
返回值:返回成功写入的对象个数,如果出现错误或到文件末尾,那么返回一个短计数值(或者 0)。
5. 格式化输入文件流
fscanf 函数用于从一个文件流中格式化读取数据,fscanf 函数在遇到空格和换行符的时候就会结束。前面我们说了 IIO 框架下的 sysfs 文件内容都是字符串,因此我们在读取的时候就需要使用字符串读取格式。在这里就可以使用 fscanf 函数来格式化读取文件内容,函数原型如下:int fscanf(FILE *stream, const char *format, ,[argument...])
fscanf用法和scanf类似,函数参数和返回值含义如下:
stream:要操作的文件流。
format:格式。
argument:保存读取到的数据。
返回值:成功读取到的数据个数,如果读到文件末尾或者读取错误就返回 EOF。
6. 程序源码
该部分程序目的是连续不断读取传感器数据,在程序中,传感器数据保存在xx文件中,通过file_data_read函数,去获取传感器数据。
/*
* @description : 读取指定文件内容
* @param - filename : 要读取的文件路径
* @param - str : 读取到的文件字符串
* @return : 0 成功;其他 失败
*/
static int file_data_read(char *filename, char *str)
{
int ret = 0;
FILE *data_stream;
data_stream = fopen(filename, "r"); /* r:只读打开 */
if(data_stream == NULL) {
printf("can't open file %s\r\n", filename);
return -1;
}
//fscanf:格式化读取文件内容---IIO框架下的sysfs文件内容都是字符串的形式,因此读取时需用字符串的读取方式
ret = fscanf(data_stream, "%s", str);
if(!ret) {
printf("file read error!\r\n");
} else if(ret == EOF) {
/* 读到文件末尾的话将文件指针重新调整到文件头 */
fseek(data_stream, 0, SEEK_SET);
}
fclose(data_stream); /* 关闭文件 */
return 0;
}
程序主要流程如下,fopen函数将文件以只读打开返回文件流指针到data_stream,data_stream为该文件的的唯一句柄,后续操作都需通过该句柄访问文件;fscanf函数格式化读取文件内容到str;最后通过fclose关闭文件。
二. 读取adc数据
读取adc数据部分主要由调用其他函数完成,程序如下:
/*
* @description : 获取adc数据
* @param - dev : 设备结构体
* @return : 0 成功;其他 失败
*/
static int sensor_read(struct adc_dev *dev)
{
int ret = 0;
char str[50];
SENSOR_FLOAT_DATA_GET(ret, IN_VOLTAGE_SCALE, str, scale);
SENSOR_INT_DATA_GET(ret, IN_VOLTAGE3_RAW, str, raw);
/* 转换得到实际的电压值 */
dev->act = (dev->scale * dev->raw)/1000.f;
return ret;
}
其中,SENSOR_FLOAT_DATA_GET和SENSOR_INT_DATA_GET函数实际为调用上一章节读取文件流数据函数,且其中文件路径file_path[index]需与sensor_read函数中文件索引对应。
/* 字符串转数字,将浮点小数字符串转换为浮点数数值 */
#define SENSOR_FLOAT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atof(str);\ //将字符串转化为浮点数
/* 字符串转数字,将整数字符串转换为整数数值 */
#define SENSOR_INT_DATA_GET(ret, index, str, member)\
ret = file_data_read(file_path[index], str);\
dev->member = atoi(str);\ //将字符串转化为整数
/* adc iio框架对应的文件路径 */
static char *file_path[] = {
"/sys/bus/iio/devices/iio:device0/in_voltage_scale",
"/sys/bus/iio/devices/iio:device0/in_voltage3_raw",
};
/* 文件路径索引,要和file_path里面的文件顺序对应 */
enum path_index {
IN_VOLTAGE_SCALE = 0,
IN_VOLTAGE3_RAW,
};
三. 应用主程序
理解完上述两个函数的程序原理,接下来的main函数编写就十分简单了。在main函数中,在循环内一直调用sensor_read函数,并将获得的数据储存在结构体adc内,然后不断打印出结构体内的值。
/*
* adc数据设备结构体
*/
struct adc_dev{
int raw;
float scale;
float act;
};
struct adc_dev adc;
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int ret = 0;
int fd;
if (argc != 1) {
printf("Error Usage!\r\n");
return -1;
}
open("/dev/adc", O_RDWR); //O_RDWR:打开驱动程序读和写
if(fd < 0)
{
printf("open error\n");
return -1;
}
while (1) {
ret = sensor_read(&adc);
if(ret == 0) { /* 数据读取成功 */
printf("\r\n原始值:\r\n");
printf("ADC原始值:%d,电压值:%.3fV\r\n", adc.raw, adc.act);
/* %.3f:格式化输出字符串,以规范的格式输出float型,占宽3列右对齐
不足三位在前面有空格补齐,多于三位以实际宽度输出。小数点也占1位。
*/
}
usleep(100000); /*100ms */
}
return 0;
}
至此,通过文件流方式读取数据App应用程序编写完成。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)