设备树 + 内核驱动适配:机械臂外设、编码器、限位开关配置
Linux内核不认识你的硬件——设备树就是那张"身份证",驱动才能"认人办事"。
一、设备树在机械臂系统中的作用
嵌入式Linux系统跑在机械臂的上位机板子上(比如树莓派、NXP i.MX8、RK3568),这块板子连着PWM输出、GPIO输入、SPI编码器、I2C姿态传感器等一堆外设。问题是:内核怎么知道板子上接了什么硬件、接在哪个引脚上?
设备树(Device Tree,DTS/DTSI)就是回答这个问题的。它是一份硬件描述文件,用树状结构记录:
- 板上有哪些设备
- 设备挂在哪条总线下面
- 用了哪些引脚、哪些时钟、哪些中断
- 设备的私有配置参数
内核启动时解析设备树,为每个节点找到匹配的驱动程序,调用驱动的probe()函数完成初始化。这个过程叫做"从设备树到驱动probe"的流程:
DTS文件 → dtc编译 → DTB二进制 → 内核启动解析 → 遍历节点 → 匹配compatible字符串 → 调用driver.probe() → probe()中获取资源(GPIO/SPI/I2C/中断号) → 初始化设备二、DTS节点编写基础
一个DTS节点的标准结构:
device_name@address { compatible = "vendor,device-model"; // 驱动匹配的关键字 reg = <0x44 0x100>; // 设备地址 + 寄存器长度 status = "okay"; // okay=启用, disabled=禁用 pinctrl-0 = <&device_pins>; // 引脚复用配置 clocks = <&clk_per>; // 使用的时钟源 interrupt-parent = <&gpio0>; // 中断控制器 interrupts = <5 IRQ_TYPE_EDGE_FALLING>; // 中断号 + 触发类型 vendor-custom-prop = <1000>; // 设备私有属性 };compatible是最关键的属性——内核用它做字符串匹配,找到对应的platform_driver。格式建议"厂商,型号",避免和其他设备冲突。
三、机械臂外设DTS配置实例
3.1 PWM输出配置(关节电机控制)
机械臂每个关节需要一个PWM通道控制电机转速/扭矩:
&pwm0 { status = "okay"; pinctrl-0 = <&pwm0_ch0_pins &pwm0_ch1_pins>; pinctrl-names = "default"; // PWM私有配置:周期和极性 joint1_pwm { compatible = "arm,pwm-motor"; pwms = <&pwm0 0 50000 0>; // PWM0通道0, 周期50ms(20Hz), 正极性 duty-cycle = <25000>; // 默认50%占空比 }; joint2_pwm { compatible = "arm,pwm-motor"; pwms = <&pwm0 1 50000 0>; // PWM0通道1 duty-cycle = <25000>; }; };pwms属性格式:<&PWM控制器 通道号 周期(ns) 极性(0正/1负)>。驱动在probe时通过of_property_read_variable_u32_array()解析这些值。
3.2 GPIO输入配置(限位开关检测)
限位开关是机械臂的安全底线——关节运动到极限位置时触发,必须立即停止电机:
/ { limit_switches { compatible = "arm,limit-switch"; status = "okay"; joint1_limit_low { gpios = <&gpio0 5 GPIO_ACTIVE_LOW>; // GPIO0_5, 低电平触发 interrupt-parent = <&gpio0>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; // 下降沿中断 debounce-ms = <10>; // 硬件防抖时间 label = "Joint1 Lower Limit"; }; joint1_limit_high { gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; interrupt-parent = <&gpio0>; interrupts = <6 IRQ_TYPE_EDGE_FALLING>; debounce-ms = <10>; label = "Joint1 Upper Limit"; }; }; };限位开关用GPIO_ACTIVE_LOW是因为机械臂限位开关通常常态断开(高电平),触发时闭合拉低(低电平)。中断用下降沿,即开关触发瞬间立刻捕获。
3.3 SPI配置(绝对编码器读取)
绝对编码器通过SPI接口上报关节角度值,每个关节挂一个编码器:
&spi0 { status = "okay"; pinctrl-0 = <&spi0_pins>; cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>, // CS0: 关节1编码器 <&gpio0 11 GPIO_ACTIVE_LOW>, // CS1: 关节2编码器 <&gpio0 12 GPIO_ACTIVE_LOW>; // CS2: 关节3编码器 spi-max-frequency = <10000000>; // 10MHz SPI时钟 encoder_j1: aksc@0 { compatible = "akm,aksc-absolute-encoder"; reg = <0>; // SPI片选0号 spi-cpha = <1>; // SPI模式1(CPHA=1, CPOL=0) spi-cpol = <0>; resolution = <14>; // 14位精度(16384位置/圈) }; encoder_j2: aksc@1 { compatible = "akm,aksc-absolute-encoder"; reg = <1>; spi-cpha = <1>; spi-cpol = <0>; resolution = <14>; }; };SPI模式要和编码器数据手册一致。resolution是私有属性,驱动用它计算角度换算系数。
3.4 I2C配置(姿态传感器IMU)
IMU(如MPU6050/BNO085)提供机械臂末端姿态数据:
&i2c1 { status = "okay"; pinctrl-0 = <&i2c1_pins>; clock-frequency = <400000>; // I2C 400kHz Fast Mode imu: mpu6050@68 { compatible = "invensense,mpu6050"; reg = <0x68>; // I2C地址(7位) interrupt-parent = <&gpio1>; interrupts = <15 IRQ_TYPE_LEVEL_LOW>; // 数据就绪中断 mount-matrix = <0 1 0>, <1 0 0>, <0 0 -1>; // 安装方向矩阵 }; };mount-matrix描述IMU芯片的物理安装方向与机械臂坐标系的映射关系。这个矩阵如果不配,姿态数据坐标系就歪了——你以为是水平,实际是倾斜45度。
四、pinctrl引脚复用:避免"引脚打架"
SoC的引脚通常有多个复用功能(GPIO/SPI/PWM/I2C…),同一引脚不能同时做两件事。pinctrl子系统负责在DTS中声明每个设备要用哪些引脚以及哪种复用模式:
&pinctrl { spi0_pins: spi0-pins { pins = "PIN10", "PIN11", "PIN12"; function = "spi0"; // 这些引脚复用为SPI0功能 bias-pull-up; // 上拉 drive-strength = <8>; // 8mA驱动能力 }; pwm0_ch0_pins: pwm0-ch0 { pins = "PIN20"; function = "pwm0"; // PIN20复用为PWM输出 }; i2c1_pins: i2c1-pins { pins = "PIN30", "PIN31"; function = "i2c1"; }; };常见坑:不同设备声明了同一个引脚但复用功能不同,内核只认后加载的那个。调试时如果发现某个外设 mysteriously 不工作,先查pinctrl有没有冲突。
五、限位开关驱动:中断触发 + 防抖
限位开关驱动的核心逻辑:
staticirqreturn_tlimit_switch_isr(intirq,void*dev_id){structlimit_switch_data*data=dev_id;intgpio_val=gpio_get_value(data->gpio);// 软件防抖:延迟100ms后再次确认if(gpio_val==data->active_level){// 确认触发:立即停止该关节电机emergency_stop_joint(data->joint_id);dev_info(data->dev,"Joint%d limit triggered!\n",data->joint_id);}returnIRQ_HANDLED;}staticintlimit_switch_probe(structplatform_device*pdev){structdevice_node*np=pdev->dev.of_node;structlimit_switch_data*data;data=devm_kzalloc(&pdev->dev,sizeof(*data),GFP_KERNEL);// 从设备树获取GPIO号data->gpio=of_get_named_gpio(np,"gpios",0);devm_gpio_request_one(&pdev->dev,data->gpio,GPIOF_IN,"limit-switch");// 从设备树获取中断号data->irq=gpio_to_irq(data->gpio);devm_request_irq(&pdev->dev,data->irq,limit_switch_isr,IRQF_TRIGGER_FALLING,"limit-switch",data);return0;}devm_*系列函数是资源管理的"自动回收"版本——设备移除时自动释放GPIO和中断,不会遗漏。
六、编码器驱动:输入捕获读取脉冲
增量编码器驱动通过输入捕获模式计算转速和位置:
staticintencoder_probe(structplatform_device*pdev){structdevice_node*np=pdev->dev.of_node;structencoder_data*data;data=devm_kzalloc(&pdev->dev,sizeof(*data),GFP_KERNEL);// 获取SPI设备(绝对编码器用SPI读取)data->spi=of_find_spi_device_by_node(np);// 获取分辨率属性of_property_read_u32(np,"resolution",&data->resolution);// 换算系数:360度 / (2^resolution) = 每个计数的角度data->angle_per_count=360.0/(1<<data->resolution);return0;}// 读取编码器角度值statics32encoder_read_angle(structencoder_data*data){u8 tx_buf[2]={0x00,0x00};// 读命令u8 rx_buf[2];structspi_transfert={.tx_buf=tx_buf,.rx_buf=rx_buf,.len=2,};spi_sync_transfer(data->spi,&t,1);// 14位数据: rx_buf[0]的高6位 + rx_buf[1]的8位u16 raw=((rx_buf[0]&0x3F)<<8)|rx_buf[1];returnraw*data->angle_per_count;// 返回角度(0~360度)}七、完整DTS配置:6轴机械臂外设树
一个6轴机械臂上位机的完整DTS骨架:
/ { model = "6-Axis Robotic Arm Controller"; compatible = "arm,robot-controller"; // 6路PWM关节电机输出 &pwm0 { ... joint1_pwm ~ joint6_pwm ... }; // 12路GPIO限位开关(每轴2个) limit_switches { ... 12个开关节点 ... }; // 6路SPI绝对编码器 &spi0 { ... encoder_j1 ~ encoder_j6 ... }; // I2C IMU姿态传感器 &i2c1 { imu: mpu6050@68 { ... }; }; // UART与下位机通信 &uart3 { status = "okay"; }; // pinctrl引脚复用声明 &pinctrl { spi0_pins, pwm0_pins, i2c1_pins, ... }; };实际项目中DTS文件可能很长(数百行),建议按功能拆分为多个.dtsi文件(pwm.dtsi、encoder.dtsi、imu.dtsi),主DTS用#include组合。
八、常见配置错误排查
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| probe未调用 | compatible不匹配 | 检查DTS和驱动compatible字符串是否一致 |
| GPIO读不到 | pinctrl冲突 | 用cat /sys/kernel/debug/pinctrl/pinctrl-maps查看引脚分配 |
| SPI通信失败 | CS片选未声明 | 检查cs-gpios属性是否完整 |
| 中断不触发 | 中断类型错误 | 限位开关通常是EDGE_FALLING,检查interrupts属性 |
| IMU数据方向错 | mount-matrix未配 | 根据实际安装方向配置3x3旋转矩阵 |
一个终极排查命令:dtc -I dtb -O dts /sys/firmware/devicetree/base—— 直接从运行中的系统反编译出设备树,看看内核实际拿到的是什么,和你的源文件对比,99%的问题都能找到根因。