☰
嵌入式Linux驱动开发:三星6818平台HC-SR04超声波测距驱动实现
2026/10/16 4:34:50 网站建设 项目流程

1. 项目缘起与整体设计思路

嵌入式Linux驱动开发里,超声波测距算是非常经典的入门级外设项目。它涉及GPIO操作、中断处理、高精度计时、字符设备框架、sysfs接口等多个核心知识点,麻雀虽小五脏俱全。这次我拿到的需求是在三星6818芯片平台上,从零编写一套超声波测距驱动,最终要能通过用户态程序稳定读取距离值,精度控制在厘米级。

三星6818这颗芯片在工业控制和嵌入式教学领域用得比较多,它属于Cortex-A53八核架构,主频能跑到1.4GHz以上,外设资源相当丰富。选它来做超声波驱动,主要看中的是它GPIO控制灵活、定时器精度足够、中断响应快这几个特点。超声波模块我用的是最常见的HC-SR04,这个模块便宜、资料多、时序简单,Trig触发加Echo回响,算一下高电平持续时间就能换算出距离。

整个方案的核心思路是这样的:驱动层负责GPIO的Trig引脚输出触发脉冲,同时监听Echo引脚的电平变化,利用内核高精度定时器测量Echo高电平的持续时间,然后通过字符设备接口把距离值上报给用户态。为什么不用输入子系统或者IIO框架?因为超声波测距本质上是一个“触发-测量-读取”的同步过程,字符设备的read接口最贴合这个交互模型,用户态一个read调用就能拿到距离值,简单直接。

注意:HC-SR04的Echo引脚输出是5V电平,而6818的GPIO是3.3V电平,直接连接会烧引脚。必须加电平转换电路,或者至少用电阻分压。这个坑我在第一次调试时就踩过,烧了一个GPIO口,心疼了好几天。

从软件架构上,我把驱动拆成三层:硬件抽象层负责GPIO和定时器的底层操作,逻辑层处理测距时序和距离计算,接口层提供字符设备节点和sysfs属性。这样分层的好处是,以后换不同的超声波模块或者换平台,只需要改硬件抽象层,逻辑层和接口层基本不用动。

关于距离计算公式,HC-SR04的数据手册写得很清楚:距离 = 高电平时间 × 声速 / 2。声速在常温下约340m/s,换算成每微秒0.034厘米。所以如果Echo高电平持续了580微秒,距离就是580 × 0.034 / 2 ≈ 9.86厘米。这里除以2是因为声音先传到物体再反射回来,走的是双程。

温度对声速有影响,公式是v = 331.4 + 0.607 × T(T是摄氏度)。如果项目对精度要求高,可以加一个温度传感器做补偿。不过一般室内场景,温度变化不大,用340m/s的固定值误差也就百分之一二,够用了。

2. 硬件连接与关键参数解析

2.1 引脚分配与电气特性

6818的GPIO分组管理,每组有若干个引脚。我选的是GPIOC组的一个引脚做Trig输出,GPIOD组的一个引脚做Echo输入。为什么这么选?因为这两个组的引脚在开发板上引出来了,接线方便,而且它们的中断能力都支持双边沿触发,这对测量Echo脉宽至关重要。

Trig引脚配置为输出模式,初始电平拉低。Echo引脚配置为输入模式,使能上拉电阻,同时注册双边沿中断。双边沿中断的意思是上升沿和下降沿都会触发中断服务程序,上升沿记录起始时间戳,下降沿记录结束时间戳,两者相减就是高电平持续时间。

提示:Echo引脚一定要使能上拉。HC-SR04在未测距时Echo输出低电平,但如果模块没供电或者线路断开,引脚会浮空,读到的值随机跳变。上拉电阻能保证浮空时读到确定的高电平,方便判断模块是否在线。

2.2 定时器精度选择

6818平台上有多种定时器可用,我选的是内核的高精度定时器框架,也就是hrtimer。为什么不用传统的jiffies或者低精度定时器?因为超声波测距的时间分辨率要求很高。HC-SR04的测量范围是2cm到400cm,对应的高电平时间是58微秒到11.6毫秒。如果用jiffies,在默认的HZ=250配置下,一个jiffy是4毫秒,根本测不准。

hrtimer的精度是纳秒级的,实际能达到微秒级,完全满足需求。我用的是ktime_get()函数获取时间戳,它返回的是纳秒精度的ktime_t类型。在中断服务程序里调用ktime_get(),记录上升沿和下降沿的时间差,然后除以1000换算成微秒。

这里有个细节要注意:中断服务程序里不能做浮点运算,内核态默认不保存浮点寄存器。所以距离计算要么用定点数,要么把原始时间值上报到用户态再算。我选择在内核态用整数运算完成计算,把微秒数乘以34再除以2000,得到的就是厘米数。34是0.034的百倍,2000是2的千倍,这样避免了浮点。

2.3 中断触发方式对比

Echo引脚的中断触发方式有三种选择:上升沿触发、下降沿触发、双边沿触发。我最终选了双边沿触发,理由如下表所示:

触发方式实现复杂度测量精度资源占用适用场景
上升沿触发低差少只测脉冲起始,需配合轮询
下降沿触发低差少只测脉冲结束,需配合轮询
双边沿触发中好中完整测量脉宽,推荐

双边沿触发的实现逻辑是:在中断处理函数里判断当前引脚电平,如果是高电平说明是上升沿,记录起始时间;如果是低电平说明是下降沿,计算时间差并唤醒等待队列。这样一次中断处理就完成了整个测量,不需要轮询,CPU占用也低。

3. 驱动核心代码实现与逐段拆解

3.1 设备结构体定义与初始化

先定义驱动私有结构体,把用到的资源都放进去:

struct ultrassonic_dev { struct cdev cdev; dev_t devno; struct class *cls; struct device *dev; int trig_gpio; int echo_gpio; int echo_irq; ktime_t start_time; ktime_t end_time; u64 pulse_us; int distance_cm; struct mutex lock; wait_queue_head_t wq; int data_ready; bool measuring; };

这个结构体里,trig_gpio和echo_gpio是GPIO编号,echo_irq是申请到的中断号,start_time和end_time记录时间戳,pulse_us是脉宽微秒数,distance_cm是最终距离。mutex用来保护并发访问,wait_queue_head用于阻塞读取,data_ready标志表示有新数据可读。

初始化流程分几步:申请GPIO、配置GPIO方向、申请中断、初始化互斥锁和等待队列、注册字符设备。每一步都要检查返回值,出错就回滚。GPIO申请用gpio_request(),配置方向用gpio_direction_output()和gpio_direction_input(),中断用request_irq()。

注意:request_irq()的最后一个参数是传递给中断处理函数的私有数据,一定要传设备结构体指针,这样在中断里才能访问到start_time等成员。我见过有人传NULL,然后在中断里用全局变量,结果多个设备就冲突了。

3.2 中断服务程序的关键逻辑

中断服务程序是整个驱动的核心,代码不长但逻辑要严谨:

static irqreturn_t echo_irq_handler(int irq, void *dev_id) { struct ultrassonic_dev *us = dev_id; int level = gpio_get_value(us->echo_gpio); if (level) { us->start_time = ktime_get(); } else { us->end_time = ktime_get(); us->pulse_us = ktime_to_us(ktime_sub(us->end_time, us->start_time)); if (us->pulse_us > 0 && us->pulse_us < 30000) { us->distance_cm = (int)(us->pulse_us * 34 / 2000); us->data_ready = 1; wake_up_interruptible(&us->wq); } } return IRQ_HANDLED; }

这段代码有几个关键点。第一,用gpio_get_value()判断当前电平,高电平说明是上升沿,低电平说明是下降沿。第二,ktime_sub()计算时间差,ktime_to_us()换算成微秒。第三,加了范围过滤,脉宽超过30000微秒(对应约5米)认为是无效测量,丢弃。第四,wake_up_interruptible()唤醒阻塞在read上的用户进程。

为什么上限设30000微秒?HC-SR04标称最大测距4米,对应脉宽约23.5毫秒。留点余量设30毫秒,超过这个值基本是干扰或者模块故障。下限没设太严,因为近距离测量脉宽很短,设下限反而会漏掉有效数据。

3.3 触发测距与读取接口

用户态通过read接口读取距离,但读之前需要先触发一次测距。我的设计是:read调用时自动触发测距,然后阻塞等待数据就绪。这样用户态一个read就完成“触发-等待-读取”全过程,用起来最方便。

触发测距的函数很简单:先把Trig拉低至少2微秒,再拉高至少10微秒,然后拉低。HC-SR04检测到10微秒以上的高电平就会发射超声波。代码:

static void trigger_measurement(struct ultrassonic_dev *us) { gpio_set_value(us->trig_gpio, 0); udelay(2); gpio_set_value(us->trig_gpio, 1); udelay(12); gpio_set_value(us->trig_gpio, 0); }

udelay(12)是保证高电平至少10微秒,多给2微秒余量。为什么先拉低2微秒?确保Trig引脚有一个干净的低电平起始状态,避免上次操作的残留电平影响模块识别。

read函数的实现:

static ssize_t ultrassonic_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct ultrassonic_dev *us = filp->private_data; int ret; if (count < sizeof(int)) return -EINVAL; mutex_lock(&us->lock); us->data_ready = 0; trigger_measurement(us); mutex_unlock(&us->lock); ret = wait_event_interruptible_timeout(us->wq, us->data_ready, HZ); if (ret == 0) return -ETIMEDOUT; if (ret < 0) return ret; if (copy_to_user(buf, &us->distance_cm, sizeof(int))) return -EFAULT; return sizeof(int); }

wait_event_interruptible_timeout的超时设成HZ,也就是1秒。正常测距几十毫秒就完成了,1秒超时足够宽松。如果超时说明模块没响应,返回-ETIMEDOUT让用户态知道出问题了。

3.4 sysfs属性与调试接口

除了read接口,我还加了几个sysfs属性方便调试。在/sys/class/ultrassonic/ultrassonic0/目录下,可以cat distance查看最近一次距离,cat pulse_us查看原始脉宽,echo 1 > trigger手动触发一次测距。

sysfs属性的实现用DEVICE_ATTR宏:

static ssize_t distance_show(struct device *dev, struct device_attribute *attr, char *buf) { struct ultrassonic_dev *us = dev_get_drvdata(dev); return sprintf(buf, "%d\n", us->distance_cm); } static DEVICE_ATTR_RO(distance);

这些属性在调试阶段特别有用。比如怀疑模块没工作时,可以先echo 1 > trigger,然后cat pulse_us看有没有值。如果pulse_us一直是0,说明Echo引脚没收到回响,可能是接线问题或者模块坏了。

4. 实操调试与问题排查实录

4.1 完整调试流程

驱动编译成ko模块后,insmod加载,然后用mknod创建设备节点,或者依赖udev自动创建。加载成功后,dmesg应该能看到驱动打印的初始化信息,包括申请的GPIO编号和中断号。

第一步先验证GPIO控制是否正常。用sysfs的gpio接口手动拉高拉低Trig引脚,用万用表或者示波器测量实际电平。这一步能排除GPIO申请失败、引脚被其他驱动占用等问题。

第二步验证中断是否触发。把Echo引脚接3.3V再接地,模拟上升沿和下降沿,看dmesg有没有中断打印。如果中断没触发,检查request_irq的返回值,以及中断触发方式是否配置正确。

第三步接上HC-SR04模块,手动触发一次测距,观察pulse_us的值。正常应该在几百到几万微秒之间。如果一直是0,用示波器看Echo引脚有没有波形。有波形但中断没触发,说明中断配置有问题;没波形,说明模块没工作或者Trig信号没到位。

第四步用用户态程序连续读取,测试稳定性和精度。拿卷尺量实际距离做对比,校准计算参数。

4.2 常见问题速查表

现象可能原因排查方法解决方案
加载模块失败GPIO被占用cat /sys/kernel/debug/gpio换引脚或释放占用
中断不触发触发方式错误检查request_irq参数改为双边沿触发
pulse_us始终为0Echo接线问题示波器测Echo波形检查接线和电平转换
距离值跳变严重电源干扰示波器看电源纹波加滤波电容
读取超时模块未响应手动触发看pulse_us检查模块供电
距离偏差大声速参数不准对比实际距离调整计算系数
多次读取冲突并发未保护多进程同时read加互斥锁

4.3 独家避坑经验

第一个坑是电平匹配。前面提过,HC-SR04的Echo是5V输出,6818的GPIO是3.3V。我一开始直接用导线连,结果GPIO口烧了。后来用两个电阻分压,1k和2k串联,5V经过分压后是3.3V,问题解决。Trig引脚倒是可以直连,因为6818输出的3.3V高电平,HC-SR04能识别为高电平。

第二个坑是中断上下文里不能睡眠。我一开始在中断处理函数里用msleep()等稳定,结果内核报错。中断上下文只能用udelay()或者ndelay()这种忙等待,不能用会睡眠的函数。后来把延时逻辑移到工作队列里处理。

第三个坑是多次测距之间的间隔。HC-SR04两次测距之间至少要间隔60毫秒,否则上次的回波还没消失,会影响下次测量。我在trigger_measurement()里加了msleep(60),但注意这个函数是在进程上下文调用的,可以睡眠。

第四个坑是并发访问。如果两个进程同时read,会同时触发测距,时序就乱了。加互斥锁保护整个触发和等待过程,确保同一时间只有一个测距在进行。

第五个坑是距离计算的整数溢出。pulse_us是u64类型,乘以34之后可能溢出int。所以计算时先用u64算,最后再转int。30000微秒乘以34是1020000,没超过int范围,但养成好习惯用u64中间变量。

提示:调试阶段可以在中断处理函数里加printk打印时间戳,但正式版本一定要去掉。中断里printk会影响实时性,高频中断下甚至会导致系统卡顿。用trace_printk或者ftrace替代更好。

5. 性能优化与扩展方向

5.1 测量稳定性优化

基础版本跑通后,我发现距离值偶尔会跳变,特别是测量较远物体时。分析下来主要是两个原因:一是环境噪声干扰,二是单次测量偶然误差。

针对噪声干扰,我在软件里加了中值滤波。连续测5次,去掉最大最小值,剩下3个取平均。这样能有效滤掉突发的干扰脉冲。实现上就是在read接口里循环触发5次,收集数据后排序取中间值。

针对偶然误差,我调整了中断的过滤阈值。原来只过滤了脉宽上限,现在下限也加了,小于100微秒的脉冲认为是电气噪声,直接丢弃。100微秒对应约1.7厘米,低于HC-SR04的最小测距2厘米,所以不会误杀有效数据。

还有一个优化是温度补偿。我在sysfs里加了一个temperature属性,用户态可以写入当前温度值,驱动根据公式动态调整声速。不写就用默认的340m/s。这个功能在温差大的环境下很有用,比如冬天室外和夏天室外,声速能差百分之十几。

5.2 多模块支持与平台移植

当前驱动只支持一个超声波模块。如果要支持多个,需要把设备结构体做成数组或者链表,每个模块独立管理自己的GPIO和中断。字符设备可以用主设备号加次设备号区分,sysfs属性也按索引命名。

平台移植方面,GPIO操作部分用的是Linux的通用GPIO框架,换平台主要改GPIO编号和中断号。6818的GPIO编号计算方式是:组号乘以32加引脚号。比如GPIOC组的第5个引脚,编号是2×32+5=69。不同平台的编号规则不一样,移植时查数据手册确认。

定时器部分用的是hrtimer,这是内核通用接口,跨平台没问题。字符设备框架也是通用的。所以整个驱动里,真正跟6818绑定的只有GPIO编号和中断号这两处,移植工作量不大。

5.3 从测距到避障的应用延伸

超声波测距本身只是手段,最终目的是做避障或者距离监测。我在用户态写了一个简单的避障demo:循环读取距离,如果小于30厘米就点亮LED或者触发蜂鸣器。这个demo跑在6818上,响应延迟在100毫秒以内,足够应付低速移动的场景。

如果要做得更精细,可以结合多个超声波模块做扇形扫描,判断障碍物的方向和距离。或者把数据上传到上位机,做可视化显示。这些扩展都建立在稳定的驱动基础上,驱动层把距离值准确上报,上层应用怎么玩都行。

我个人在实际操作中的体会是,嵌入式驱动开发最花时间的不是写代码,而是调试硬件。GPIO电平对不对、中断触没触发、时序合不合规,这些问题用示波器一看便知,比盯着代码猜效率高得多。所以手边常备一个逻辑分析仪或者示波器,能省下大量调试时间。另外,驱动里多加sysfs调试接口,比反复insmod/rmmod改代码方便得多,这个习惯我从这个项目开始一直保持到现在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询