
内核版本Linux-4.9.88
概述
介绍:
超声波测距模块是利用超声波来测距。模块先发送超声波,然后接收反射回来的超声波,由反射经历的时间和声音的传播速度 340m/s,计算得出距离。
SR04 是一款常见的超声波传感器,模块自动发送 8 个 40KHz 的方波,自动检测是否有信号返回,用户只需提供一个触发信号,随后检测回响信号的时间长短即可。
SR04 采用 5V 电压,静态电流小于 2mA,感应角度最大约 15 度,探测距离约 2cm-450cm。
SR04 是市面上比较成熟模块,价格低廉,用户一般不用考虑重新设计硬件,直接购买使用即可,参考电路如下。
SR04 模块上面有四个引脚,分别为:VCC、Trig、Echo、GND。
⚫ Trig 是脉冲触发引脚,即控制该脚让 SR04 模块开始发送超声波。
⚫ Echo 是回响接收引脚,即 SR04 模块一旦接收到超声波的返回信号则输出回响信号,回响信号的脉冲宽度与所测距离成正比。
SR04 时序及编程思路:
时序图:
要测距,需如下操作:
① 触发:向 Trig(脉冲触发引脚)发出一个大约 10us 的高电平。
② 发出超声波,接收反射信号:模块就自动发出 8 个 40Khz 的超声波,超声波遇到障碍物后反射回来,模块收到返回来的超声波。
③ 回响:模块接收到反射回来的超声波后,Echo 引脚输出一个与检测距离成
比例的高电平。
我们只要在该引脚为高时,开启定时器计数,在该引脚变为低时,结束定时器计数。根据定时器的计数和定时器频率就可以算出经历时间,根据时间即可推导出距离。
程序
涉及重要知识点:
– GPIO设备结构体– GPIO中断
– 定时器中断
– ioctl
– poll机制
– 环形缓冲区
sr04_drv.c
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/delay.h>
#define CMD_TRIG 100
struct gpio_desc{
int gpio;
int irq;
char *name;
int key;
struct timer_list key_timer;
} ;
static struct gpio_desc gpios[2] = {
{115, 0, "trig", },
{116, 0, "echo", },
};
/* 主设备号 */
static int major = 0;
static struct class *gpio_class;
/* 环形缓冲区 */
#define BUF_LEN 128
static int g_sr04[BUF_LEN]; // 缓冲区数组
static int r, w; // 读写索引
struct fasync_struct *Sr04_fasync; // 异步通知结构体
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_sr04_buf_empty(void)
{
return (r == w);
}
static int is_sr04_buf_full(void)
{
return (r == NEXT_POS(w));
}
static void put_sr04(int sr04_val)
{
if (!is_sr04_buf_full())
{
g_sr04[w] = sr04_val;
w = NEXT_POS(w);
}
}
static int get_sr04(void)
{
int sr04_val = 0;
if (!is_sr04_buf_empty())
{
sr04_val = g_sr04[r];
r = NEXT_POS(r);
}
return sr04_val;
}
// 创建等待队列
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);
/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t sr04_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
/* 在读函数和中断里面不能添加打印 */
int err;
int sr04;
if (is_sr04_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
/* 等待缓冲区非空 */
wait_event_interruptible(gpio_wait, !is_sr04_buf_empty());
/* 从缓冲区读取数据 */
sr04 = get_sr04();
if (sr04 == -1)
{
return -EAGAIN;
}
/* 向用户程序返回数据 */
err = copy_to_user(buf, &sr04, sizeof(sr04));
return sizeof(sr04);
}
/* poll轮询 */
static unsigned int sr04_poll(struct file *fp, poll_table * wait)
{
/* 将当前进程加入到等待队列gpio_wait中,直到传感器缓冲区非空 */
poll_wait(fp, &gpio_wait, wait); //wait: 超时时间
/* 返回POLLIN | POLLRDNORM,表示可以进行读取操作。*/
return is_sr04_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
/* 异步通知 */
static int sr04_fasync(int fd, struct file *file, int on)
{
// 添加异步通知
if (fasync_helper(fd, file, on, &Sr04_fasync) >= 0)
return 0;
else
return -EIO;
}
/* ioctl函数 */
static long sr04_ioctl(struct file *filp, unsigned int command, unsigned long arg)
{
//send trig
switch(command)
{
case CMD_TRIG:
{
// 发送触发信号
gpio_set_value(gpios[0].gpio, 1);
udelay(20);
gpio_set_value(gpios[0].gpio, 0);
/* 启动定时器 -设置超时时间,避免丢失信号 */
mod_timer(&gpios[1].key_timer, jiffies + msecs_to_jiffies(50));
}
}
return 0;
}
/* 定义自己的file_operations结构体 */
static struct file_operations sr04_drv = {
.owner = THIS_MODULE,
.read = sr04_read,
.poll = sr04_poll,
.fasync = sr04_fasync,
.unlocked_ioctl = sr04_ioctl,
};
static irqreturn_t sr04_isr(int irq, void *dev_id)
{
struct gpio_desc *gpio_desc = dev_id;
int val;
static u64 rising_time = 0;
u64 time;
/* 读取gpio值 */
val = gpio_get_value(gpio_desc->gpio);
if(val)
{
/* 上升沿记录起始时间 */
rising_time = ktime_get_ns();
}
else
{
if(rising_time == 0)
{
return IRQ_HANDLED;
}
/* 下降沿记录结束时间,并计算时间间隔*/
//停止定时器
del_timer(&gpios[1].key_timer);
//计算时间间隔
time = ktime_get_ns() - rising_time;
rising_time = 0;
//放入缓冲区
put_sr04(time);
/* 通知等待队列可以读数据 */
wake_up_interruptible(&gpio_wait);
kill_fasync(&Sr04_fasync, SIGIO, POLL_IN);
}
return IRQ_HANDLED;
}
//定时器函数
static void sr04_timer_func(unsigned long arg)
{
/* 将-1放入缓冲区 */
put_sr04(-1);
//唤醒等待队列
wake_up_interruptible(&gpio_wait);
// 发送一个SIGIO信号给与sr04_fasync相关的异步I/O操作,告知它们有新的数据可读取。这里的POLL_IN表示可读事件。
kill_fasync(&Sr04_fasync, SIGIO, POLL_IN);
}
/* 在入口函数 */
static int __init sr04_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
// trig pin
err = gpio_request(gpios[0].gpio, gpios[0].name); //申请获得GPIO引脚
gpio_direction_output(gpios[0].gpio, 0); //设置为输出引脚
// echo pin
{
/* 获取gpio对应的中断号 */
gpios[1].irq = gpio_to_irq(gpios[1].gpio);
/* 申请中断 */
err = request_irq(gpios[1].irq, sr04_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpios[1].name, &gpios[1]);
/* 设置定时器 */
setup_timer(&gpios[1].key_timer, sr04_timer_func, (unsigned long)&gpios[1]);
}
/* 注册file_operations */
major = register_chrdev(0, "100ask_sr04", &sr04_drv); /* /dev/gpio_desc */
gpio_class = class_create(THIS_MODULE, "100ask_sr04_class");
if (IS_ERR(gpio_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_sr04");
return PTR_ERR(gpio_class);
}
device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "sr04"); /* /dev/sr04 */
return err;
}
/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
*/
static void __exit sr04_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(gpio_class, MKDEV(major, 0));
class_destroy(gpio_class);
unregister_chrdev(major, "100ask_sr04");
// trig pin
gpio_free(gpios[0].gpio);
// echo pin
{
free_irq(gpios[1].irq, &gpios[1]);
del_timer(&gpios[1].key_timer);
}
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(sr04_init);
module_exit(sr04_exit);
MODULE_LICENSE("GPL");
sr04_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
#include <sys/ioctl.h>
#define CMD_TRIG 100
static int fd;
/*
* ./sr04_test /dev/sr04
*
*/
int main(int argc, char **argv)
{
int val;
struct pollfd fds[1]; // poll 监听的文件描述符
int i;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR );
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
while(1)
{
/* 发出触发信号,启动定时器,如果超时就返回错误 */
ioctl(fd, CMD_TRIG);
/* poll */
fds[0].fd = fd;
fds[0].events = POLLIN;
if (1 == poll(fds, 1, 500)) // 监督fds事件,1个时间,5000ms,如果定时器超时或者有事假发生,则被唤醒。
{
/* 读4字节数据 */
if (read(fd, &val, 4) == 4)
printf("get distance: %d\n", val * 17 / 1000000);
else
printf("get distance: err\n");
}
else
{
printf("get distance poll timeout\n");
}
/* sr04必须控制一定延迟 */
sleep(1);
}
close(fd);
return 0;
}
Makefile
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88 # 板子所用内核源码的目录
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o sr04_test sr04_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order sr04_test
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += sr04_drv.o
上机测试
IMX6ULL 先断电,按下图所示,将模块插在扩展板的 GPIO0,将扩展板插在底板上。
注意:为了防止用户接错方向,模块和扩展板都有一条长白线,连接时需要模块上的白线和扩展板的白线在同一侧。
注意:注意 SR04 模块中的红线、黑线、绿线、黄线的位置,如下图接线。
编译、运行程序,打开串口观察。将手由远到近靠近超声波模块正面,可以看到如下效果,注意模块距离最小识别距离为 2cm:
评论