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作TX,GPIO0_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%的时序抖动。后来我改用HRTIMER+GPIO硬件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波形,重点检查起始位和停止位的时序。如果发现数据错位,可以尝试调整定时器的触发时机,通常提前半个时钟周期采样会更稳定。