1. STM32MP157 USART驱动架构解析
在嵌入式Linux系统中,串口通信是最基础也最重要的外设功能之一。STM32MP157作为STMicroelectronics推出的多核处理器,其USART(Universal Synchronous Asynchronous Receiver Transmitter)驱动实现遵循Linux TTY子系统架构。这套架构将硬件操作抽象为统一的接口,使得应用程序可以通过标准的设备文件(如/dev/ttyS0)访问串口,而无需关心底层硬件差异。
整个驱动架构分为三个关键层次:
- TTY核心层:提供统一的字符设备接口和行规程管理
- UART核心层:实现串口通用操作和协议处理
- 硬件驱动层:完成具体的寄存器操作和中断处理
这种分层设计使得驱动开发更加模块化,上层应用无需修改即可适配不同硬件平台。下面我们将深入分析每个关键环节的实现细节。
2. USART驱动注册机制
2.1 驱动数据结构构建
USART驱动的注册过程始于stm32-usart.c中的初始化代码。驱动开发者需要准备两个核心数据结构:
c复制static struct uart_driver stm32_usart_driver = {
.driver_name = "stm32-usart",
.dev_name = "ttyS",
.major = 0, // 动态分配主设备号
.minor = 0, // 起始次设备号
.nr = 6, // 支持的串口数量
.cons = NULL,
};
static struct uart_ops stm32_usart_ops = {
.tx_empty = stm32_usart_tx_empty,
.set_mctrl = stm32_usart_set_mctrl,
.get_mctrl = stm32_usart_get_mctrl,
.start_tx = stm32_usart_start_tx,
.stop_tx = stm32_usart_stop_tx,
.startup = stm32_usart_startup,
.shutdown = stm32_usart_shutdown,
// 其他操作函数...
};
uart_driver结构体定义了驱动的基本信息,而uart_ops则包含了硬件相关的操作函数集合。这两个结构体通过uart_register_driver()和uart_add_one_port()函数注册到内核中。
注意:STM32MP157的USART驱动支持DMA传输模式,这需要在ops中额外实现
dma_rx_config和dma_tx_config等回调函数。
2.2 TTY设备节点创建
注册完成后,内核会在/dev目录下创建对应的设备节点。以USART1为例,通常会创建/dev/ttyS0设备文件。设备节点的创建过程涉及以下步骤:
- 内核根据
uart_driver中的dev_name和nr参数确定设备名称和数量 - 通过
tty_register_driver()在TTY子系统中注册驱动 - 使用
device_create()在devtmpfs中创建设备节点
设备节点的权限通常为crw-rw----,主次设备号可以通过ls -l /dev/ttyS*命令查看。
2.3 硬件相关配置
在驱动注册阶段,还需要完成以下硬件特定配置:
- 时钟使能:通过
clk_prepare_enable()激活USART外设时钟 - 引脚复用:配置GPIO为USART功能模式
- 中断注册:设置接收/发送中断处理函数
- DMA通道配置(如果使用DMA模式)
这些配置通常在平台设备(platform_device)的probe函数中完成,与设备树(Device Tree)中的节点定义相匹配。
3. USART设备打开流程剖析
3.1 设备节点到驱动匹配
当应用程序调用open("/dev/ttyS0", O_RDWR)时,内核通过以下路径找到对应的驱动:
- 根据设备号(主设备号+次设备号)在
tty_drivers链表中查找匹配的tty_driver - 找到
stm32_usart_driver对应的tty_driver实例 - 分配并初始化
tty_struct结构体,保存当前打开的终端状态
这个过程由tty_open_by_driver()函数实现,核心代码如下:
c复制static struct tty_struct *tty_open_by_driver(dev_t device, struct inode *inode,
struct file *filp)
{
struct tty_driver *driver;
int index;
// 查找匹配的tty_driver
driver = tty_lookup_driver(device, filp, &index);
if (!driver)
return ERR_PTR(-ENODEV);
// 分配tty_struct
tty = tty_init_dev(driver, index);
tty->ops = driver->ops;
// 调用行规程的open函数
retval = tty_ldisc_setup(tty, tty->link);
// 调用uart_ops中的open函数
if (tty->ops->open)
retval = tty->ops->open(tty, filp);
return tty;
}
3.2 硬件初始化过程
打开操作最终会调用到硬件驱动的startup函数(stm32_usart_startup),完成以下硬件初始化:
-
配置USART寄存器:
- 波特率(BRR寄存器)
- 数据位/停止位/校验位(CR1/CR2寄存器)
- 使能接收器和发送器(CR1寄存器)
-
中断配置:
- 使能RXNE(接收缓冲区非空中断)
- 使能TC/TXE(发送完成/发送缓冲区空中断)
- 在DMA模式下配置DMA中断
-
DMA通道准备(如果使用DMA):
- 分配DMA缓冲区
- 配置DMA源/目标地址
- 设置传输长度和方向
典型的startup函数实现如下:
c复制static int stm32_usart_startup(struct uart_port *port)
{
struct stm32_port *stm32_port = to_stm32_port(port);
// 使能时钟
clk_prepare_enable(stm32_port->clk);
// 配置GPIO
pinctrl_pm_select_default_state(port->dev);
// 配置USART参数
stm32_usart_set_bits(port, USART_CR1, UE | RE | TE);
stm32_usart_set_baudrate(port, 115200);
// 注册中断处理函数
ret = request_irq(port->irq, stm32_usart_interrupt,
IRQF_NO_SUSPEND, "stm32-usart", port);
// 初始化DMA
if (stm32_port->dma_mode) {
stm32_usart_dma_rx_init(stm32_port);
stm32_usart_dma_tx_init(stm32_port);
}
return 0;
}
3.3 行规程初始化
在打开过程中,内核还会初始化行规程(Line Discipline),默认使用n_tty(即常规TTY模式)。行规程负责处理特殊字符(如Ctrl+C)和缓冲管理,其初始化流程包括:
- 分配行规程结构体
- 设置输入/输出缓冲区
- 注册操作函数集(
n_tty_ops)
行规程使得串口设备可以像终端一样工作,支持行编辑、回显等功能。
4. 数据读取机制详解
4.1 中断驱动接收流程
STM32 USART的数据接收主要依赖中断机制。当RXNE(接收缓冲区非空)标志置位时,硬件触发中断,执行以下流程:
- 中断处理函数
stm32_usart_interrupt()被调用 - 读取USART_SR寄存器状态,判断中断类型
- 对于接收中断,从USART_DR寄存器读取数据
- 将数据存入
tty_port的环形缓冲区 - 唤醒等待数据的进程
关键的中断处理代码如下:
c复制static irqreturn_t stm32_usart_interrupt(int irq, void *ptr)
{
struct uart_port *port = ptr;
unsigned int sr;
sr = stm32_usart_read(port, USART_SR);
// 处理接收中断
if (sr & USART_SR_RXNE) {
unsigned char c = stm32_usart_read(port, USART_DR);
uart_insert_char(port, sr, USART_SR_ORE, c, TTY_NORMAL);
}
// 处理发送中断
if (sr & USART_SR_TXE) {
stm32_usart_tx_interrupt(port);
}
// 处理错误中断
if (sr & USART_SR_ERROR_MASK) {
handle_error(port, sr);
}
return IRQ_HANDLED;
}
4.2 用户空间读取路径
当应用程序调用read()系统调用时,数据从硬件到用户空间的完整路径如下:
- 用户调用
read(fd, buf, len) - 内核通过VFS调用
tty_read() n_tty_read()从行规程缓冲区获取数据- 如果缓冲区为空,进程进入睡眠状态(等待队列)
- 当硬件接收到数据并触发中断后,数据被存入缓冲区并唤醒进程
- 数据从内核空间复制到用户空间缓冲区
这个过程中涉及两个重要的缓冲区:
- 硬件FIFO:USART内置的小容量缓冲区(通常1-8字节)
- 软件环形缓冲区:
tty_port中的缓冲区(默认4KB)
4.3 DMA接收模式
在高速或大数据量场景下,可以使用DMA接收模式提高效率:
- 配置DMA控制器自动从USART_DR读取数据
- 设置DMA完成中断,在缓冲区满或超时时处理数据
- 使用循环DMA模式实现零拷贝接收
DMA模式的关键配置包括:
c复制static void stm32_usart_dma_rx_init(struct stm32_port *stm32_port)
{
struct dma_slave_config dma_conf = {
.src_addr = port->mapbase + USART_DR,
.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.direction = DMA_DEV_TO_MEM,
};
// 配置DMA通道
dmaengine_slave_config(stm32_port->rx_ch, &dma_conf);
// 准备DMA缓冲区
sg_init_one(&stm32_port->rx_sg, stm32_port->rx_buf, RX_BUF_L);
// 提交DMA传输
stm32_usart_submit_rx_dma(stm32_port);
}
注意事项:DMA模式下需要特别注意缓冲区对齐和缓存一致性问题,可能需要使用
dma_alloc_coherent()分配内存。
5. 数据写入机制深入分析
5.1 写入操作调用链
用户空间write()系统调用经过以下路径到达硬件:
write(fd, buf, len)->vfs_write()tty_write()->n_tty_write()uart_write()->__uart_start()stm32_usart_start_tx()(硬件驱动中的发送函数)
关键的函数调用关系如下:
c复制static const struct tty_operations uart_ops = {
.write = uart_write,
// 其他操作...
};
static const struct tty_port_operations uart_port_ops = {
.dtr_rts = uart_dtr_rts,
.activate = uart_port_activate,
.shutdown = uart_port_shutdown,
};
static const struct uart_ops stm32_usart_ops = {
.start_tx = stm32_usart_start_tx,
// 其他硬件操作...
};
5.2 中断驱动发送
默认的中断驱动发送流程:
- 用户数据首先被复制到
tty_port的发送缓冲区 - 驱动启用TXE(发送缓冲区空中断)
- 当USART发送寄存器为空时,触发中断
- 中断处理函数从缓冲区取出数据写入USART_DR
- 重复步骤3-4直到所有数据发送完成
发送中断处理的关键代码:
c复制static void stm32_usart_tx_interrupt(struct uart_port *port)
{
struct circ_buf *xmit = &port->state->xmit;
// 检查是否有数据需要发送
if (uart_circ_empty(xmit)) {
stm32_usart_disable_tx_interrupt(port);
return;
}
// 从缓冲区取出数据写入USART
stm32_usart_write(port, USART_DR, xmit->buf[xmit->tail]);
xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
// 检查是否发送完成
if (uart_circ_empty(xmit))
stm32_usart_disable_tx_interrupt(port);
}
5.3 DMA发送模式
对于大数据量传输,DMA发送模式可以显著降低CPU负载:
- 配置DMA控制器自动将数据写入USART_DR
- 设置DMA传输完成中断
- 使用
dmaengine_prep_slave_sg()准备分散/聚集传输 - 启动DMA传输后,USART硬件自动从DMA缓冲区获取数据
DMA发送初始化示例:
c复制static void stm32_usart_dma_tx_init(struct stm32_port *stm32_port)
{
struct dma_slave_config dma_conf = {
.dst_addr = port->mapbase + USART_DR,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.direction = DMA_MEM_TO_DEV,
};
dmaengine_slave_config(stm32_port->tx_ch, &dma_conf);
}
static int stm32_usart_dma_tx(struct uart_port *port)
{
struct stm32_port *stm32_port = to_stm32_port(port);
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_sg(stm32_port->tx_ch,
&stm32_port->tx_sg,
1, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT);
dmaengine_submit(desc);
dma_async_issue_pending(stm32_port->tx_ch);
return 0;
}
5.4 流控与发送阻塞
在实际应用中,需要考虑硬件流控(RTS/CTS)和软件流控(XON/XOFF)对发送过程的影响:
- 当流控信号指示对方不可接收时,发送操作应该阻塞
- 在驱动中需要检查
tty_port->flags中的ASYNC_CTS_FLOW等标志 - 使用
tty_port_set_flow()函数控制流控行为
发送阻塞的实现通常依赖于tty_port->lock和等待队列机制。
6. 性能优化与调试技巧
6.1 中断与DMA模式选择
根据应用场景选择合适的数据传输模式:
| 模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 中断模式 | 低速率、小数据量 | 实现简单、响应快 | CPU占用率高 |
| DMA模式 | 高速率、大数据量 | 降低CPU负载、支持零拷贝 | 配置复杂、延迟较高 |
实际项目中,可以动态切换模式:当数据量小于阈值时使用中断模式,大于阈值时切换到DMA模式。
6.2 缓冲区大小调优
优化缓冲区大小对性能有显著影响:
- 接收缓冲区:默认4KB,对于高速通信可以增大到16KB或更大
- 发送缓冲区:需要平衡内存使用和吞吐量
- DMA缓冲区:应该与硬件FIFO大小匹配,通常为2的幂次方
可以通过sysfs接口动态调整缓冲区大小:
bash复制# 查看当前缓冲区大小
cat /sys/class/tty/ttyS0/buffer_size
# 设置新的缓冲区大小
echo 16384 > /sys/class/tty/ttyS0/buffer_size
6.3 常见问题排查
-
数据丢失:
- 检查中断处理函数是否及时读取USART_DR
- 确认DMA配置是否正确,特别是缓冲区地址和长度
- 提高接收中断优先级(
IRQF_NO_SUSPEND标志)
-
发送卡死:
- 检查流控信号是否正常
- 确认TXE中断是否被正确启用/禁用
- 使用示波器测量实际信号波形
-
波特率误差:
- STM32的USART波特率计算公式:
baud = f_ck / (8*(2-over8)*USARTDIV) - 使用高精度外部时钟源(如25MHz晶体)
- 在设备树中正确配置时钟分频
- STM32的USART波特率计算公式:
6.4 调试工具推荐
-
内核打印:启用
CONFIG_SERIAL_STM32_CONSOLE和动态调试c复制#define dev_dbg(dev, fmt, ...) \ dynamic_dev_dbg(dev, fmt, ##__VA_ARGS__) -
逻辑分析仪:使用Saleae或DSLogic捕获实际信号
-
procfs接口:查看串口状态和统计信息
bash复制cat /proc/tty/driver/serial -
strace工具:跟踪系统调用
bash复制strace -e trace=write,read /path/to/application
7. 实际项目经验分享
在工业现场部署STM32MP157 USART驱动时,我总结了以下实战经验:
-
抗干扰设计:
- 在PCB布局时,USART信号线远离高频信号
- 添加TVS二极管保护电路
- 在软件中实现帧校验(CRC)和重传机制
-
低功耗优化:
- 空闲时关闭USART时钟
- 使用DMA唤醒机制替代持续中断
- 动态调整波特率(当通信间隔较长时)
-
多串口管理:
- 为每个串口分配独立的中断优先级
- 使用工作队列处理非实时任务
- 实现端口间的流量控制和优先级调度
-
与用户空间交互:
- 通过
ioctl()实现自定义控制命令 - 使用
poll()/select()实现多路复用 - 开发专用的
sysfs接口用于状态监控
- 通过
一个典型的工业应用初始化流程如下:
c复制int init_industrial_uart(struct stm32_port *port)
{
// 1. 基本串口配置
stm32_usart_set_baudrate(port, 115200);
stm32_usart_set_mode(port, CS8 | CSTOPB | PARENB);
// 2. 硬件流控使能
stm32_usart_set_flow(port, FLOW_CTRL_RTS_CTS);
// 3. DMA配置
if (port->dma_capable) {
stm32_usart_dma_rx_init(port);
stm32_usart_dma_tx_init(port);
}
// 4. 错误检测使能
stm32_usart_enable_error_interrupt(port);
// 5. 注册看门狗定时器
setup_timer(&port->watchdog, uart_watchdog, (unsigned long)port);
return 0;
}
在长期运行稳定性方面,建议添加以下保护机制:
- 接收超时监控(看门狗定时器)
- 错误统计和自动恢复
- 热插拔检测和支持
- 动态时钟校准(应对温度变化)
通过以上分析和实践经验,开发者可以更好地理解和优化STM32MP157的USART驱动,满足各种嵌入式场景下的串口通信需求。