Linux下GPIO模拟UART驱动的实现与优化

张开发
2026/4/16 23:03:00 15 分钟阅读

分享文章

Linux下GPIO模拟UART驱动的实现与优化
1. 为什么需要GPIO模拟UART驱动在嵌入式开发中UART串口通信是最常用的外设接口之一。但实际项目中经常遇到这样的尴尬主控芯片的硬件UART接口数量有限而需要连接的串口设备却很多。比如工业控制场景中可能需要同时连接多个RS485传感器智能家居网关要对接多个Modbus设备机器人控制器要处理多个电机驱动器的反馈信号。这时候硬件UART接口就捉襟见肘了。重新选型芯片成本太高外扩UART芯片又增加BOM成本。其实Linux内核强大的GPIO子系统配合高精度定时器完全可以用软件方式模拟出UART通信协议。我去年做的一个智能农业项目就遇到这种情况主控的3个硬件UART都被占用了但还需要接入土壤传感器的485信号最终就是用GPIO模拟的方案完美解决。2. GPIO初始化与配置2.1 GPIO引脚选择要点选GPIO引脚时要注意三点首先查看芯片手册避免复用特殊功能的引脚其次优先选择支持中断的GPIO这对接收数据很关键最后两组GPIO最好物理位置相邻方便布线。比如我常用GPIO0_12作TXGPIO0_13作RX这两个引脚在开发板上就是相邻的。具体初始化代码要注意几个细节#define TX_PIN GPIO_TO_PIN(0, 12) #define RX_PIN GPIO_TO_PIN(0, 13) // 初始化TX为推挽输出 gpio_request(TX_PIN, uart_tx); gpio_direction_output(TX_PIN, 1); // 默认高电平 // 初始化RX为输入并配置中断 gpio_request(RX_PIN, uart_rx); gpio_direction_input(RX_PIN); irq_num gpio_to_irq(RX_PIN); request_irq(irq_num, rx_handler, IRQF_TRIGGER_FALLING, uart_irq, NULL);2.2 电气特性调优模拟UART要特别注意信号质量。我在实际测试中发现长距离传输时需要在GPIO输出端加上10K上拉电阻同时并联100pF电容滤波。如果通信不稳定可以适当降低波特率或者调整GPIO的驱动强度通过芯片的GPIO_DRV寄存器配置。3. 高精度定时器实现波特率控制3.1 定时器参数计算波特率的本质是位周期比如9600bps对应每个位104us。Linux的高分辨率定时器(HRTIMER)精度可以达到纳秒级完全满足需求。关键是要处理好定时器重载和误差累积问题static enum hrtimer_restart tx_timer_callback(struct hrtimer *timer) { // 发送下一位数据 transmit_next_bit(); // 重新计算下次触发时间 ktime_t period ktime_set(0, 1000000000/baudrate); hrtimer_forward_now(timer, period); return HRTIMER_RESTART; }3.2 实测中的时序优化在树莓派4B上实测发现单纯用HRTIMER会有约2%的时序抖动。后来我改用HRTIMERGPIO硬件PWM结合的方式用PWM产生基准时钟用HRTIMER做微调这样可以将抖动控制在0.5%以内。具体实现时要根据具体芯片调整有的SoC支持硬件波形发生器就更简单了。4. 中断与数据接收处理4.1 中断服务程序设计接收端的中断服务程序要尽可能短小精悍。我的经验是只做三件事记录时间戳、禁用中断防抖动、激活底半部处理static irqreturn_t rx_isr(int irq, void *dev_id) { ktime_t now ktime_get(); fifo_put(time_fifo, now); // 时间戳入队 disable_irq_nosync(irq); tasklet_schedule(rx_tasklet); // 触发底半部 return IRQ_HANDLED; }4.2 自适应波特率检测对于未知设备可以实现波特率自动检测。我的做法是在中断中测量起始位下降沿到第一个上升沿的时间计算出大概波特率范围然后尝试常见波特率9600/19200/38400等直到收到有效数据帧。这个方法在对接不同厂家的PLC设备时特别有用。5. 数据缓存与流量控制5.1 双缓冲FIFO设计为了避免数据丢失我设计了两级缓冲硬件中断层用循环缓冲驱动层用内核kfifo。实测在115200bps下这种结构可以承受单次突发10ms的数据而不丢失。关键代码如下struct { uint8_t buffer[1024]; uint16_t head; uint16_t tail; spinlock_t lock; } hw_fifo; struct kfifo sw_fifo; static void process_rx_data(void) { uint8_t byte; while(hw_fifo.head ! hw_fifo.tail) { byte hw_fifo.buffer[hw_fifo.tail]; kfifo_put(sw_fifo, byte, 1); hw_fifo.tail (hw_fifo.tail 1) % 1024; } }5.2 流量控制策略当缓冲区超过75%容量时可以通过拉高某个GPIO模拟CTS信号通知对方暂停发送。我在项目中扩展了这个机制当检测到持续过载时会自动动态调整缓冲区大小最大可扩展到16KB。6. 驱动与用户层交互6.1 字符设备接口将模拟UART注册为标准tty设备后用户层就可以用标准串口API操作了。关键是要实现好tty_operations结构体中的回调函数static const struct tty_operations uart_ops { .open uart_open, .close uart_close, .write uart_write, .write_room uart_write_room, .poll uart_poll, //... };6.2 select/poll机制实现为了让应用层能高效处理数据到达事件需要实现poll函数。这里有个技巧在中断处理中调用wake_up_interruptible()唤醒等待队列static unsigned int uart_poll(struct file *file, poll_table *wait) { unsigned int mask 0; poll_wait(file, read_queue, wait); if(!kfifo_is_empty(sw_fifo)) mask | POLLIN | POLLRDNORM; return mask; } // 在中断处理中 if(kfifo_len(sw_fifo) 0) wake_up_interruptible(read_queue);7. 性能优化实战技巧经过多个项目验证我总结出几个关键优化点首先启用CONFIG_PREEMPT_RT实时补丁可以将延时抖动降低80%其次为GPIO中断设置最高优先级最后在驱动加载时调用preempt_disable()禁止抢占。在IMX6ULL平台上经过这些优化后模拟UART在115200bps下的误码率可以做到低于10^-6。调试时可以用逻辑分析仪抓取GPIO波形重点检查起始位和停止位的时序。如果发现数据错位可以尝试调整定时器的触发时机通常提前半个时钟周期采样会更稳定。

更多文章