
简介这套资源定位于嵌入式Linux驱动开发者与物联网通信开发者围绕SX1276/77/78/79芯片实现了一个带IEEE 802.15.4 MAC接口的LoRa内核模块包含驱动源码、设备树覆盖板与应用测试程序可用来学习如何在Linux系统中接入低功耗广域无线通信能力。压缩包共10个文件、仅17KB由3个C源文件、3个Makefile、3个Markdown文档和1个DTS文件组成C代码实现驱动与测试逻辑Makefile负责构建MD文档补充使用说明DTS描述LoRa外设在设备树中的挂载方式。目前已有四百八十四人学习浏览是一份体量虽小但链路完整的LoRa驱动参考资料。资源包将内核侧驱动、设备树描述和应用层测试组织为三个相互对应的部分读者可以对照阅读内核模块如何注册为网络设备、如何通过设备树挂载LoRa外设以及用户态程序如何完成收发测试。对于正在尝试LoRa驱动迁移、设备树移植或MAC层驱动接管的开发者这份源码提供了可直接构建和验证的起点。1. 基于Linux的LoRa内核模块不是写不了用户态而是项目要求字符设备三周前有个项目把 LoRa 模块接到一块 i.MX6ULL 板卡上模块型号是 SX1268。厂商给的资料是用户态 C 库通过 /dev/spidev 操作芯片。开始跑点对点透传没问题但一上多进程并发、中断接收、内核态定时唤醒用户态库就露怯了设备节点不存在、收发状态要靠轮询、任务调度一卡就丢包。换路子把 LoRa 驱动写成了 Linux 内核模块注册成字符设备应用层只做 read/write/ioctl权责一下子清晰了。这篇文章就把这套「基于Linux的LoRa内核模块设备驱动程序源码应用测试程序源码」怎么落地、怎么调试、哪些坑绕不开完整讲一遍。适合正在嵌入式 Linux 上调 LoRa、又不想用厂商黑匣子库的人。2. LoRa 内核模块驱动设计字符设备骨架、SPI 通信与关键数据结构2.1 为什么选字符设备而不是 netdev 或 SerdevLoRa 不是以太网设备虽然有射频收发但数据帧格式是私有协议没有标准网络栈接口。用网络设备netdev接入内核意味着要处理 SKB、协议类型、ARP 等一堆无关逻辑不值。用串行设备框架serdev适合接 UART 型 LoRa 模块但大多数 SX1276 / SX1268 是 SPI 接口所以字符设备是最直接的抽象。字符设备的核心价值是三点第一能创建稳定的 /dev/lorax 设备节点应用层可以用 open/close/read/write/ioctl 统一操作第二内核态可以直接注册中断LoRa 的接收完成中断能做到微秒级响应比用户态轮询省 CPU第三可以用内核的 waitqueue 让应用层阻塞读收不到包时进程睡眠不占 CPU。常见做法是使用miscdevice框架这种框架不需要手动分配主设备号注册后自动挂到 misc 类下设备节点会出现在 /dev 下。如果设备有多个实例或者需要动态编号就老老实实注册字符设备区域并用class_create创建设备节点。我一般用 miscdevice一个板子上就一两个 LoRa 模块够用且代码简洁。2.2 驱动源码的核心骨架module_init、misc_register 与 file_operations写内核模块第一步是确定 file_operations 里实现哪些回调。LoRa 设备最少要实现open打开设备时初始化release释放资源read阻塞读取一帧数据write把数据写入 LoRa FIFO 并触发发送ioctl设置频率、扩频因子、带宽、发射功率等参数以及llseek或者直接返回-ESPIPE不让定位文件偏移。下面是一个最小可用的骨架。注意这里省去了具体芯片寄存器的定义实际项目中你会把寄存器映射放在头文件里方便芯片型号切换。#include linux/module.h #include linux/miscdevice.h #include linux/fs.h #include linux/uaccess.h #include linux/spi/spi.h #include linux/interrupt.h #include linux/mutex.h #include linux/semaphore.h #include linux/delay.h #define DRIVER_NAME lora_sx #define LORA_CMD_SET_FREQUENCY 0x01 #define LORA_CMD_SET_MODULATION 0x02 struct lora_dev { struct spi_device *spi; struct mutex lock; struct semaphore data_ready; // 通知应用读取数据 unsigned char rx_buf[255]; unsigned int rx_len; }; static struct lora_dev *lora_instance; static int lora_open(struct inode *inode, struct file *filp) { filp-private_data lora_instance; return 0; } static ssize_t lora_read(struct file *filp, char __user *buf, size_t len, loff_t *offs) { struct lora_dev *dev filp-private_data; int ret; // 等待数据信号量 if (down_interruptible(dev-data_ready)) return -ERESTARTSYS; mutex_lock(dev-lock); ret copy_to_user(buf, dev-rx_buf, dev-rx_len); if (ret) { mutex_unlock(dev-lock); return -EFAULT; } mutex_unlock(dev-lock); return dev-rx_len; } static ssize_t lora_write(struct file *filp, const char __user *buf, size_t len, loff_t *offs) { // 实际工程里这里是把数据从用户态拷贝到内核缓冲区 // 然后调用 lora_send_packet() 写 FIFO、拉高 TX 引脚、等待发送完成中断。 return len; } static long lora_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct lora_dev *dev filp-private_data; unsigned long value; if (copy_from_user(value, (void __user *)arg, sizeof(value))) return -EFAULT; switch (cmd) { case LORA_CMD_SET_FREQUENCY: dev_dbg(dev-spi-dev, set frequency to %lu Hz\n, value); // 写频率寄存器 break; case LORA_CMD_SET_MODULATION: dev_dbg(dev-spi-dev, set modulation parameter\n); break; default: return -EINVAL; } return 0; } static const struct file_operations lora_fops { .owner THIS_MODULE, .open lora_open, .read lora_read, .write lora_write, .unlocked_ioctl lora_ioctl, }; static int lora_spi_probe(struct spi_device *spi) { struct lora_dev *dev; dev kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; mutex_init(dev-lock); sema_init(dev-data_ready, 0); dev-spi spi; spi_set_drvdata(spi, dev); lora_instance dev; // 这里需要做芯片初始化reset引脚拉低再拉高 // 等待芯片Ready然后写寄存器配置 LoRa 模式。 return 0; } static int lora_spi_remove(struct spi_device *spi) { struct lora_dev *dev spi_get_drvdata(spi); kfree(dev); return 0; } static const struct of_device_id lora_of_match[] { { .compatible semtech,sx1268 }, {} }; MODULE_DEVICE_TABLE(of, lora_of_match); static struct spi_driver lora_spi_driver { .driver { .name DRIVER_NAME, .of_match_table lora_of_match, }, .probe lora_spi_probe, .remove lora_spi_remove, }; module_spi_driver(lora_spi_driver); MODULE_LICENSE(GPL);这段代码展示的是 SPI 驱动的标准骨架重点在 file_operations 和 misc 设备之间的关联我没有完全展开。实际工程里你还需要一个misc_register的调用一般放在probe函数里。为什么用module_spi_driver而不是module_init直接注册因为 LoRa 是 SPI 从机驱动必须依赖 SPI 控制器设备树节点。用module_spi_driver后probe 会在设备树匹配时自动执行不需要你自己维护加载顺序。注意上面代码里lora_instance用了全局指针这在内核模块里是禁忌只能用于教学。实际项目应该把设备实例挂到filp-private_data并且在 open 时通过container_of从 inode 获取。否则一旦有第二个设备功能就乱了。2.3 与 LoRa 芯片通信SPI 读写寄存器与时序约束LoRa 芯片的 SPI 是半双工、先写地址字节再读数据。SX1268 的命令格式是命令名 参数比如0x06是 WriteRegister。写寄存器时要先发0x06然后写寄存器地址高字节、低字节最后写数据。很多坑出在片选拉低后的第一个字节就错了导致后面所有数据都偏位。常见驱动写法里SPI 通信用一个spi_transfer结构体不允许用多个spi_transfer拼成一条消息因为 LoRa 芯片往往要求片选在整个传输过程中保持低电平。分开传输会导致片选在中间拉高芯片复位通信状态机。我一般把命令头、地址、数据和回读数据全部拼到一个spi_message里一次传输完成。static int reg_write(struct spi_device *spi, u16 addr, u8 val) { u8 buf[4]; struct spi_transfer tr { .tx_buf buf, .len 4 }; struct spi_message msg; buf[0] 0x06; // WriteRegister buf[1] (addr 8) 0xFF; buf[2] addr 0xFF; buf[3] val; spi_message_init(msg); spi_message_add_tail(tr, msg); return spi_sync(spi, msg); }这个函数看起来简单但有三个隐藏参数。第一是 SPI 的 modeLoRa 芯片一般支持 SPI Mode 0CPOL0, CPHA0也有的模块需要 Mode 0 或 Mode 0/1具体看数据手册。第二是 max_speed_hz我一般先设 1MHz跑通后再往上调。很多模块调 10MHz 会偶发读回 0xFF那是时序裕量不够不是芯片坏了。第三是 bits_per_word默认 8 位别改成别值。设备树里对应节点要这样声明ecspi1 { lora_sx12680 { compatible semtech,sx1268; reg 0; spi-max-frequency 1000000; }; };注意 reg 0 对应片选引脚编号同一个 SPI 总线上接了多个设备时reg 要对应硬件 CS 线。我在一个板子上踩过 reg 和实际 GPIO 片选不一致导致通信完全不通的问题后来查设备树才知道这个 reg 不是随便写的必须和 SPI 控制器 cs-gpios 数组里的索引对应。3. 从零写一个可用的 LoRa 驱动寄存器配置、收发路径与 ioctl 参数解析3.1 初始化 LoRa 芯片复位脉冲与关键寄存器配置序列LoRa 芯片上电后是未知状态必须先复位。SX1268 复位是拉低 NRST 引脚至少 100us 再拉高然后等芯片 busy 引脚变低。有些模块没有把 busy 引脚接出来那就只能延时等 5ms。复位后第一步是检查芯片版本号比如读0x42B0寄存器应该返回一个非零值如果读回来全是 0xFF说明 SPI 通信没建立。之后配置LoRa 模式。SX1268 默认是 FSK 模式要切换到 LoRa 模式需要往LoRaSyncWord和ModulationParams写参数。这里用SetPacketType命令0x8A设为 LoRa。接着配置频率、扩频因子、带宽、编码率。这些值不是一拍脑袋写的它们共同决定了空中速率和灵敏度。带宽越大速率越高但灵敏度越低扩频因子越大抗干扰越好但速率越低。项目里常用 SF7/125kHz 做城市内近距离抄表SF10/125kHz 做远距离低速率传感器。具体初始化顺序static int lora_init_chip(struct lora_dev *dev) { // 1. 复位 gpio_set_value(dev-reset_gpio, 0); usleep_range(200, 300); gpio_set_value(dev-reset_gpio, 1); msleep(5); // 2. 切到 LoRa 模式 // SetPacketType: 0x8A, 0x01LoRa lora_write_cmd(dev, 0x8A, 0x01); // 3. 配置调制参数 // SetModulationParams: 0x8B // 参数: SF7, BW125kHz, CR4/5, LDRO0 u8 mod_params[4] { 7 4, 0x02, 0x04, 0 }; // 具体编码看芯片手册 lora_write_cmd(dev, 0x8B, mod_params, 4); // 4. 配置包参数 // SetPacketParams: 0x8C // 固定长度或可变长度、CRC、前导码长度等 u8 pkt_params[6] { 0, 0, 0, 0x0C, 0, 0 }; lora_write_cmd(dev, 0x8C, pkt_params, 6); // 5. 设置频率为 470MHz示例 lora_set_frequency(dev, 470000000); // 6. 设置发射功率22dBm 需要打开 PA 模式 lora_write_cmd(dev, 0x8E, 0x16); // SetTxParams: max power, ramp time return 0; }这段代码里的lora_write_cmd是命令级封装需要把命令和参数放到一个缓冲区一次性发出去。参数的具体数值要对照 SX1268 datasheet不同批次芯片可能支持不同的带宽编码。我开发时习惯把SetModulationParams的参数打印到内核日志里方便对照逻辑分析仪抓到的波形。3.2 发送路径从用户态 write 到 FIFO 到射频发射中断用户态调用 write 后的完整路径是内核模块把数据从用户空间拷贝到内核缓冲区调用芯片的SetTx命令把数据填入 FIFO然后拉高 TX 触发发射。SX1268 的 FIFO 只有 255 字节超过就要分帧。实际项目里我会在 ioctl 里预留一个LORA_CMD_SET_TX_LEN让应用层明确知道单帧上限。发送完成通常有TxDone中断。处理器在中断里拿到标志后要用GetIrqStatus命令读取中断状态并清除否则下次中断会丢失。这里有个大坑中断里调用 SPI 读写是可以的但绝对不能用等待的同步信号量否则会死锁或者睡眠在中断上下文。我一般用workqueue把后续处理交给进程上下文。static irqreturn_t lora_dio0_irq(int irq, void *data) { struct lora_dev *dev data; // 只把工作队列挂上去快速返回 schedule_work(dev-work_rx); return IRQ_HANDLED; }3.3 接收路径RxDone 中断、读 FIFO 与 read 实现接收是 LoRa 驱动里优先保证的功能。SX1268 收到有效包时会拉高 DIO1 引脚触发中断。中断服务里要做三件事关接收中断防止重入读取 FIFO 数据唤醒在等待数据的进程。数据读取必须在中断返回前完成因为芯片 FIFO 在下一包到来时可能被覆盖。我的实现是把数据先放到驱动的环形缓冲区然后用up(dev-data_ready)唤醒读线程。用户态 read 走的是阻塞等待信号量的逻辑有数据就 copy_to_user没有就睡在进程上下文中。如果应用层需要非阻塞读可以在 file_operations 里检查filp-f_flags O_NONBLOCK对应返回-EAGAIN。注意中断注册时的 flag 参数。LoRa 的 DIO1 如果是高电平触发并且有上拉电阻用IRQF_TRIGGER_RISING。如果芯片在休眠状态时 DIO1 不上拉板子设计容易误触发建议配合IRQF_NO_SUSPEND防止系统挂起时丢中断。3.4 ioctl 射频参数控制频率、带宽、扩频因子、CRC 的映射应用层调 LoRa 参数最方便的是 ioctl。我定义了一组指令GET_VERSION、SET_FREQUENCY、SET_SF_BW_CR、SET_TX_POWER、CALIBRATE。参数用struct lora_param { u32 freq_hz; u8 sf; u8 bw_khz; u8 cr; u8 crc_en; }这种结构应用层填好内核模块做范围校验后转换成寄存器值。为什么不用 sysfssysfs 适合单值读写多字段配置太啰嗦。ioctl 虽然古老但够直白。关键校验点频率不能越过芯片支持的允许范围中国大陆 LoRa 常用 470-510MHz 或 779-787MHz扩频因子只能在 6 到 12 之间带宽必须落在芯片支持的档位125kHz/250kHz/500kHz。如果校验不过就返回 -EINVAL避免非法配置让芯片进错误状态。这里有一个发散点很多应用同时用多组参数做信道切换比如一个设备在 470MHz 下发指令在 478MHz 上报数据。驱动如果每次切换都要重写一整组寄存器会有几毫秒盲区。我的做法是在 ioctl 里加入LORA_CMD_PREPARE_CHANNEL预先配置好一组完整参数再用LORA_CMD_SWITCH_TO_CHANNEL只写频率寄存器切换时间从 5ms 降到 0.2ms。这个优化在 TDMA 场景里很有用。4. 应用测试程序源码验证驱动的所有功能路径4.1 测试程序的结构设备节点、初始化、收发线程写测试程序的目标不是展示应用业务逻辑而是覆盖驱动每个接口。我的测试程序分四个模块参数解析模块命令行读取频率、SF、发送间隔、设备初始化模块open 一组 ioctl 配置、发送线程周期发送递增序号数据包、接收线程阻塞 read 并打印时间戳和数据。这样能同时验证驱动在双向并发下的行为。设备节点通常默认是/dev/lora开机后由 udev 创建。测试程序开头检查 open 是否成功如果 open 失败大概率是模块没加载或者设备节点权限不足。测试程序里我建议显式调用close后再重新 open观察驱动是否泄漏内存这个方法能发现绝大多数probe和release不配对的问题。4.2 发送与接收的代码示例以下是我在项目中用过的测试代码核心部分可以直接编译跑起来。注意这里ioctl的参数传递方式必须用arg而不是直接传arg。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #define LORA_IOC_SET_FREQ _IOW(L, 1, unsigned int) #define LORA_IOC_SET_SF _IOW(L, 2, unsigned int) #define LORA_IOC_START_RX _IO(L, 3) #define LORA_IOC_STOP_RX _IO(L, 4) int main(int argc, char **argv) { int fd open(/dev/lora, O_RDWR | O_NONBLOCK); if (fd 0) { perror(open /dev/lora); return -1; } unsigned int freq 470000000; ioctl(fd, LORA_IOC_SET_FREQ, freq); unsigned int sf 7; ioctl(fd, LORA_IOC_SET_SF, sf); ioctl(fd, LORA_IOC_START_RX); unsigned char tx_buf[32]; for (int i 0; i 16; i) { snprintf(tx_buf, sizeof(tx_buf), seq:%d, i); int len strlen(tx_buf) 1; int ret write(fd, tx_buf, len); if (ret ! len) perror(write); usleep(100000); } ioctl(fd, LORA_IOC_STOP_RX); close(fd); return 0; }这段代码的意图是发送 16 帧短数据接收端在另一台设备上监听。测试时要注意O_NONBLOCK的效果如果驱动里 read 也是非阻塞实现的接收线程需要轮询但这不符合低功耗场景需求。我更推荐测试程序里接收线程用select加超时超时后打印“未收到数据”这样驱动阻塞读写得是否正确立刻就能看出来。4.3 编译与运行交叉编译、模块加载、日志观察内核模块交叉编译是最容易栽跟头的地方。模块必须和内核版本一致用$(KDIR)指向目标板正在运行的内核源码树不能用主机的 Ubuntu 内核头文件。我的 Makefile 如下KDIR : /home/user/workdir/kernel-source # 目标板对应内核源码树 CROSS_COMPILE : arm-linux-gnueabihf- obj-m : lora_drv.o lora_drv-objs : lora_main.o lora_spi.o all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) clean加载模块用insmod lora_drv.ko之后看/var/log/messages或dmesg输出。如果看到Unknown symbol说明模块内依赖了未导出的内核符号比如spi_register_controller之类的通常是因为用的内核模块 API 和内核版本不匹配。卸载用rmmod lora_drv但注意如果应用还拿着设备 fd卸载会报-EBUSY。测试程序里要先把 fd 释放掉。日志观察我习惯打开内核动态 debugecho file lora_spi.c p /sys/kernel/debug/dynamic_debug/control。这样能看到每次 SPI 传输的实际数据不用接逻辑分析仪就能排查大部分数据偏移问题。5. LoRa 内核驱动避坑指南加载、时序、中断与并发排查5.1 加载与编译类常见问题现象一insmod报Invalid module format或disagrees about version of symbol原因内核模块版本魔法与当前内核不匹配。最典型的场景是编译模块用的内核源码树目录里没有先执行make modules_prepare或者使用了不同分支的源码树。另一个隐藏原因是编译机和目标机的内核配置不一样比如一个开启了CONFIG_MODVERSIONS另一个没开。解决把模块编译放到目标板上进行如果板子有编译器或者严格同步内核源码树、配置文件和编译产物。现象二加载成功但dmesg里没有任何设备注册信息 原因SPI 设备树节点没有匹配。probe 函数没被调用因为设备树里 compatible 没有对应of_match_table里的字符串。解决检查设备树节点compatible semtech,sx1268是否完全一致。我之前遇到过一个板子因为compatible字符串结尾多了个空格内核匹配失败浪费了一个下午。现象三/dev/lora节点创建不了 原因miscdevice 框架会在/sys/class/misc下自动创建设备但/dev下的节点需要 udev 或devtmpfs支持。开发板上如果内核没开CONFIG_DEVTMPFS_MOUNT是不会自动挂载 devtmpfs 的。解决手动mknod /dev/lora c $(grep lora /proc/misc | awk {print $1}) 0或者在内核配置里打开 devtmpfs。5.2 运行与通信类常见问题现象四SPI 读回数据永远是0xFF原因芯片根本没进入工作状态或者 SPI 引脚配置不对。可能性从高到低排列芯片未复位NRST 引脚拉低时间不够SPI 极性相位不匹配SPI 时钟频率过高导致时序约束不满足芯片的 GPIO 供电没使能。解决先用逻辑分析仪抓 WP0SPI_CLK、WP1SPI_MOSI、WP2MISO、WP3CS四根线确认片选和时钟关系。然后试着把时钟降到 100kHz 重新读版本寄存器这一步能排除大批时钟问题。现象五接收中断不触发或者触发一次后再也收不到 原因最常见的两个一是中断触发类型不对SX1268 的 DIO1 在包接收完成时拉高如果是下降沿触发会一直丢事件二是驱动里没有及时ClearIrqStatus导致中断标志一直置位DIO1 一直高电平不会产生新的上升沿。解决在中断处理函数里先读GetIrqStatus再写命令清掉对应位最后再操作 FIFO。另外检查设备树里interrupts属性是否指定了正确的中断号和触发类型比如gpio5 3 IRQ_TYPE_EDGE_RISING。现象六并发收发时用户态进程卡死在 read 上 原因驱动里如果read使用down_interruptible等待信号量但发送路径在互斥锁里占着信号量或者信号量初值为 0 而up从未被调用进程就会永远睡死。解决先测试只收不发、只发不收确定基本路径正常。然后并发场景下在read入口加printk打印状态再看cat /proc/sleep有没有进程 D 状态。如果驱动里用了mutex_lock又等着中断去解锁那必然死锁。现象七ioctl 一调用就 oops报unable to handle kernel NULL pointer或preemption原因应用层传入的指针没有用copy_from_user/copy_to_user直接在内核态解引用了用户空间地址。多发生在ioctl参数确实是一个整型值而不是指针的场景。解决定义 ioctl 命令时用_IOR宏服务端固定unsigned long arg作为用户地址严格走copy_from_user。另外检查ioctl的返回值如果驱动返回-EFAULT应用层要继续跑就是错误的使用方式。6. 把驱动调到可靠环回自测、逻辑分析仪验证与个人习惯驱动做到能收发只是第一步要让人放心部署需要把验证做成自动化。我的做法是写一个内核态自测函数放在/proc调解用入口。echo 1 /proc/lora_test就执行一次环回测试驱动内部把发送数据填进 FIFO同时开启接收模拟一个极短时间的回环。因为同一个芯片不能同时收发这个回环只能在芯片的 packet handler 层面模拟但足以验证 SPI 寄存器写入和中断标志清除逻辑。接着是逻辑分析仪。我推荐至少抓三段波形一是复位完成后对版本寄存器的读取时序二是发送状态下写 FIFO 的波形三是接收完成中断时读取 FIFO 的波形。重点看 CS 线在整个传输期间是否保持低防止spi_sync拆分消息导致片选抖动。抓完波形后把时序和芯片 datasheet 里 Figure 和 Timing Characteristics 对比确认 setup/hold 时间是否满足。多数稳定性问题在这一步就能暴露。我还习惯在设备树里给 LoRa 节点加pinctrl配置把 SPI 引脚复用和上下拉一次性搞定避免用户态的export gpio和驱动抢资源。驱动加载后检查/sys/kernel/debug/gpio确认 DIO1 中断引脚被正确申请这个 debugfs 是排查中断申请失败的利器。最后说一个个人习惯每次改完驱动我都会跑 10000 包收发测试统计丢包率和 RX 时序抖动。如果丢包率高于 1%先检查电源纹波再看天线匹配。能跑通这个测试我才会把驱动交给应用同事。希望这套验证方法能帮你把 LoRa 内核驱动做得更稳。希望帮到你。本文还有配套的精品资源点击获取