《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 2 章 驱动设计的硬件基础

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 2 章 驱动设计的硬件基础

参考:宋宝华 著,机械工业出版社,2015年版


2.1 处理器

2.1.1 处理器的分类

嵌入式系统中使用的处理器种类繁多,按照体系结构和应用场景可以分为以下几类:

(1)通用处理器(GPP,General Purpose Processor)

通用处理器以 ARM、x86、MIPS、PowerPC 为代表,具有较强的运算能力和丰富的外设接口,适合运行 Linux 等复杂操作系统。

复制代码
常见通用处理器系列:
├── ARM 系列
│   ├── Cortex-A(应用级):手机、平板、嵌入式 Linux
│   │   ├── Cortex-A7   ← 低功耗,如 i.MX6ULL
│   │   ├── Cortex-A9   ← 主流,如 Exynos4412、i.MX6Q
│   │   ├── Cortex-A53  ← 64位,如 树莓派3
│   │   └── Cortex-A72  ← 高性能,如 树莓派4
│   ├── Cortex-R(实时级):汽车电子、工业控制
│   └── Cortex-M(微控制器级):STM32、单片机
├── x86 系列(Intel/AMD):PC、服务器
├── MIPS 系列:路由器、网络设备
└── PowerPC 系列:工业控制、航空航天
(2)数字信号处理器(DSP,Digital Signal Processor)

DSP 专为数字信号处理优化,具有**乘加运算(MAC)**硬件单元,适合音频处理、图像处理、通信调制解调等场景。

  • 代表产品:TI TMS320 系列、ADI Blackfin 系列
  • 特点:单周期完成乘加运算,有专用的循环缓冲区和位反转寻址
(3)微控制器(MCU,Micro Controller Unit)

MCU 将 CPU、内存、外设接口集成在单一芯片上,成本低、功耗小,适合简单控制任务。

  • 代表产品:STM32(ARM Cortex-M)、AVR、PIC、8051
  • 特点:片上集成 Flash、RAM、GPIO、UART、SPI、I2C、ADC 等
(4)片上系统(SoC,System on Chip)

SoC 将处理器核心、内存控制器、各种外设控制器集成在单一芯片上,是现代嵌入式系统的主流形态。

复制代码
典型 SoC 内部结构(以 i.MX6ULL 为例):
┌─────────────────────────────────────────────────────┐
│                    i.MX6ULL SoC                      │
│  ┌──────────────┐  ┌──────────────┐                 │
│  │ ARM Cortex-A7│  │  DDR3 控制器  │                 │
│  │   528MHz     │  │              │                 │
│  └──────────────┘  └──────────────┘                 │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │
│  │ UART │ │ SPI  │ │ I2C  │ │ USB  │ │ LCD  │      │
│  └──────┘ └──────┘ └──────┘ └──────┘ └──────┘      │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐               │
│  │ GPIO │ │ ADC  │ │ PWM  │ │ CAN  │               │
│  └──────┘ └──────┘ └──────┘ └──────┘               │
└─────────────────────────────────────────────────────┘

2.1.2 ARM 处理器体系结构

由于 Linux 驱动开发主要面向 ARM 平台,宋宝华在书中重点介绍了 ARM 处理器的体系结构。

ARM 处理器的工作模式

ARM 处理器(以 ARMv7 为例)共有 8 种工作模式

模式 缩写 说明 触发条件
用户模式 USR 正常程序执行模式,权限最低 正常运行
快速中断模式 FIQ 处理高速数据传输 FIQ 中断
外部中断模式 IRQ 处理普通外部中断 IRQ 中断
管理模式 SVC 操作系统保护模式 复位/软中断 SWI
中止模式 ABT 处理内存访问异常 数据/指令预取中止
未定义模式 UND 处理未定义指令异常 执行未定义指令
系统模式 SYS 特权级用户模式 软件切换
监控模式 MON TrustZone 安全扩展 SMC 指令

与 Linux 的关系 :Linux 内核运行在 SVC(管理)模式 ,用户程序运行在 USR(用户)模式 。当用户程序执行系统调用(swi 指令)时,处理器从 USR 模式切换到 SVC 模式,进入内核空间。

ARM 寄存器组

ARM 处理器有 37 个寄存器(包含各模式下的备份寄存器),用户可见的通用寄存器为 R0~R15:

复制代码
通用寄存器(用户模式可见):
R0  ~ R3   ← 函数参数/返回值(调用者保存)
R4  ~ R11  ← 局部变量(被调用者保存)
R12 (IP)    ← 过程内调用临时寄存器
R13 (SP)    ← 栈指针(Stack Pointer)
R14 (LR)    ← 链接寄存器(Link Register,保存返回地址)
R15 (PC)    ← 程序计数器(Program Counter)

特殊寄存器:
CPSR        ← 当前程序状态寄存器(Current Program Status Register)
             [31] N(负数标志)
             [30] Z(零标志)
             [29] C(进位标志)
             [28] V(溢出标志)
             [7]  I(IRQ 禁止位)
             [6]  F(FIQ 禁止位)
             [4:0] 处理器模式位
ARM 的两种指令集

ARM 处理器支持两种指令集,可以动态切换:

复制代码
ARM 指令集:
  - 32 位固定长度指令
  - 功能强大,执行效率高
  - 适合复杂运算

Thumb/Thumb-2 指令集:
  - 16 位(Thumb)或 16/32 位混合(Thumb-2)
  - 代码密度高(相同功能代码体积更小)
  - 适合代码存储空间受限的场景

切换方式:
  BX  Rn    ← 跳转并切换指令集(Rn[0]=1 切换到 Thumb)
  BLX Rn    ← 带链接的跳转并切换

2.1.3 处理器的存储管理单元(MMU)

MMU(Memory Management Unit)是运行 Linux 的处理器必须具备的硬件单元,负责虚拟地址到物理地址的转换

复制代码
虚拟地址(VA)→ MMU → 物理地址(PA)

Linux 内存布局(ARM 32位,4GB 地址空间):
┌─────────────────────────────────┐ 0xFFFFFFFF
│         内核空间(1GB)           │
│   内核代码、数据、驱动程序         │
│   物理内存直接映射区               │
│   vmalloc 区域                   │
├─────────────────────────────────┤ 0xC0000000
│         用户空间(3GB)           │
│   应用程序代码段                  │
│   数据段、BSS 段                  │
│   堆(向上增长)                  │
│   栈(向下增长)                  │
└─────────────────────────────────┘ 0x00000000

MMU 对驱动开发的影响

c 复制代码
/* 驱动程序不能直接使用物理地址访问外设寄存器 */
/* 必须先通过 ioremap() 将物理地址映射到内核虚拟地址 */

#include <linux/io.h>

#define UART0_PHYS_BASE  0x44E09000   /* UART0 物理基地址 */
#define UART0_SIZE       0x1000       /* 映射大小 4KB */

void __iomem *uart_base;

/* 在驱动 probe 函数中映射 */
uart_base = ioremap(UART0_PHYS_BASE, UART0_SIZE);
if (!uart_base) {
    pr_err("ioremap 失败\n");
    return -ENOMEM;
}

/* 通过虚拟地址访问寄存器 */
writel(0x83, uart_base + 0x0C);   /* 写寄存器 */
u32 val = readl(uart_base + 0x14); /* 读寄存器 */

/* 驱动退出时解除映射 */
iounmap(uart_base);

2.1.4 处理器的 Cache 机制

Cache(高速缓存)是处理器与主存之间的高速缓冲存储器,用于缓解 CPU 与内存之间的速度差异。

复制代码
存储层次结构(速度从快到慢):
CPU 寄存器(< 1ns)
    ↓
L1 Cache(1~4ns,16~64KB,片内)
    ↓
L2 Cache(4~10ns,256KB~4MB,片内)
    ↓
L3 Cache(10~30ns,4MB~32MB,片内/片外)
    ↓
主存 DDR(50~100ns,GB 级)
    ↓
Flash/磁盘(μs~ms 级,TB 级)

Cache 对驱动开发的影响

驱动程序在进行 DMA 操作时,必须注意 Cache 一致性问题:

c 复制代码
/*
 * DMA 操作时的 Cache 一致性处理
 *
 * 问题:CPU 写数据到内存时,数据可能只在 Cache 中,
 *       DMA 控制器直接访问物理内存,读到的是旧数据(Cache 不一致)
 *
 * 解决方案:使用一致性 DMA 内存或手动刷新 Cache
 */

/* 方法一:分配一致性 DMA 内存(CPU 和 DMA 都能看到最新数据) */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* cpu_addr:CPU 访问用的虚拟地址 */
/* dma_handle:DMA 控制器使用的物理地址 */

/* 方法二:流式 DMA 映射(性能更好,但需要手动同步) */
dma_addr_t dma_addr = dma_map_single(dev, cpu_buf, size, DMA_TO_DEVICE);
/* DMA 传输完成后解除映射 */
dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);

2.2 存储器

2.2.1 存储器的分类

嵌入式系统中的存储器按照是否需要刷新断电是否保持数据分类:

复制代码
存储器分类树:
存储器
├── RAM(随机访问存储器,断电丢失)
│   ├── SRAM(静态 RAM)
│   │   ├── 无需刷新,速度快
│   │   ├── 用于 Cache、寄存器文件
│   │   └── 成本高,集成度低
│   └── DRAM(动态 RAM)
│       ├── 需要周期性刷新
│       ├── SDRAM → DDR → DDR2 → DDR3 → DDR4 → LPDDR4
│       └── 用于系统主存
└── ROM(只读存储器,断电保持)
    ├── Mask ROM(掩膜 ROM,出厂固化,不可修改)
    ├── EPROM(可擦除可编程 ROM,紫外线擦除)
    ├── EEPROM(电可擦除可编程 ROM,字节级擦写)
    └── Flash(闪存,块级擦写)
        ├── NOR Flash(可字节寻址,可直接执行代码,XIP)
        └── NAND Flash(块寻址,容量大,价格低,需要 FTL)

2.2.2 NOR Flash 与 NAND Flash 的对比

这是嵌入式驱动开发中最常见的两种非易失性存储器,宋宝华在书中对其进行了详细对比:

对比项 NOR Flash NAND Flash
接口 并行地址/数据总线 串行 I/O 总线
容量 较小(1MB~128MB) 较大(128MB~TB 级)
读速度 快(可直接执行代码,XIP) 较慢(需先读入 RAM)
写速度
擦除单位 扇区(64KB~128KB) 块(16KB~512KB)
坏块 极少 出厂即有坏块,需坏块管理
价格 便宜
典型用途 存储 Bootloader、小容量固件 存储文件系统、大容量数据
Linux 驱动 CFI/JEDEC 接口,MTD 子系统 NAND 控制器驱动,MTD + UBI + UBIFS

NOR Flash 的 XIP(片上执行)特性

复制代码
NOR Flash 连接方式(并行总线):
CPU 地址总线 A0~A23  ──→  NOR Flash 地址引脚
CPU 数据总线 D0~D15  ←──  NOR Flash 数据引脚
CPU 片选信号 CS       ──→  NOR Flash 片选引脚

特点:CPU 可以像访问 RAM 一样直接读取 NOR Flash 中的代码并执行
     Bootloader(如 U-Boot)通常存储在 NOR Flash 中

NAND Flash 的组织结构

复制代码
NAND Flash 存储组织(以 512MB NAND 为例):
┌─────────────────────────────────────────┐
│                  芯片                    │
│  ┌─────────────────────────────────┐    │
│  │              块 0               │    │
│  │  ┌──────────────────────────┐   │    │
│  │  │  页 0(2KB 数据 + 64B OOB)│   │    │
│  │  │  页 1(2KB 数据 + 64B OOB)│   │    │
│  │  │  ...(共 64 页)           │   │    │
│  │  └──────────────────────────┘   │    │
│  └─────────────────────────────────┘    │
│  块 1、块 2 ... 块 4095(共 4096 块)    │
└─────────────────────────────────────────┘

OOB(Out-Of-Band)区域用途:
  - ECC 校验数据(纠正位翻转错误)
  - 坏块标记(Bad Block Marker)
  - 文件系统元数据(JFFS2/UBIFS)

2.2.3 DDR SDRAM

DDR(Double Data Rate)SDRAM 是嵌入式 Linux 系统的主存,在时钟的上升沿和下降沿都传输数据,因此数据传输速率是 SDRAM 的两倍。

复制代码
DDR 发展历程:
SDRAM  → DDR1  → DDR2  → DDR3  → DDR4  → DDR5
(单倍)  (双倍)  (4倍)  (8倍)  (16倍)  (32倍)

嵌入式常用:
LPDDR3 / LPDDR4 / LPDDR4X(低功耗版本,用于手机、平板)

典型参数(DDR3-1600):
  - 时钟频率:800MHz(有效传输率 1600MT/s)
  - 总线宽度:32位(嵌入式)或 64位(PC)
  - 带宽:1600 × 32 / 8 = 6400 MB/s(32位总线)

DDR 初始化:DDR 的初始化(时序参数配置)通常由 Bootloader(U-Boot)完成,Linux 内核启动时 DDR 已经可用。驱动开发者通常不需要直接操作 DDR 控制器,但需要了解其特性以优化 DMA 传输性能。


2.2.4 存储器的地址映射

在嵌入式系统中,CPU 通过地址总线访问不同的存储器和外设,不同的地址范围映射到不同的硬件:

复制代码
典型 ARM SoC 地址映射(以 i.MX6ULL 为例):
地址范围                  映射目标
0x00000000 ~ 0x0001FFFF  片内 SRAM(128KB)
0x10000000 ~ 0x1FFFFFFF  EIM(外部总线接口,连接 NOR Flash)
0x80000000 ~ 0xFFFFFFFF  DDR3 SDRAM(最大 2GB)
0x02000000 ~ 0x020FFFFF  片内外设寄存器(UART/SPI/I2C/GPIO 等)

驱动程序通过 ioremap() 访问外设寄存器:
void __iomem *base = ioremap(0x02020000, 0x4000);  /* 映射 UART1 寄存器 */

2.3 接口与总线

2.3.1 串行通信基础概念

在介绍具体总线之前,需要理解串行通信的基本概念:

波特率(Baud Rate)与比特率(Bit Rate)

  • 波特率:每秒传输的符号数(Symbol/s)

  • 比特率:每秒传输的比特数(bit/s)

  • 对于二进制信号:波特率 = 比特率

    同步与异步

  • 同步通信:发送方和接收方共享同一时钟信号(如 SPI、I2C)

  • 异步通信:没有共享时钟,通过起始位/停止位同步(如 UART)


2.3.2 UART(通用异步收发器)

UART 是嵌入式系统中最基础、最常用的串行通信接口,主要用于调试输出低速数据传输

UART 帧格式

复制代码
UART 数据帧(8N1 格式:8位数据,无校验,1位停止位):

空闲  起始位  D0  D1  D2  D3  D4  D5  D6  D7  停止位  空闲
 1     0      数  据  位  (  低  位  先  传  )   1      1

时序图:
─────┐     ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌─────
     │     │  │  │  │  │  │  │  │  │  │  │  │  │  │  │  │  │
     └─────┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘
  空闲  起始  D0   D1   D2   D3   D4   D5   D6   D7  停止  空闲

UART 关键参数

  • 波特率:常用 9600、115200、921600 bps

  • 数据位:5、6、7、8 位(通常 8 位)

  • 校验位:无(N)、奇(O)、偶(E)

  • 停止位:1、1.5、2 位

    Linux UART 驱动框架

c 复制代码
/*
 * Linux UART 驱动基于 TTY 子系统
 * 驱动需要实现 uart_ops 结构体中的操作函数
 */
#include <linux/serial_core.h>

static struct uart_ops my_uart_ops = {
    .tx_empty     = my_uart_tx_empty,    /* 发送缓冲区是否为空 */
    .set_mctrl    = my_uart_set_mctrl,   /* 设置调制解调器控制信号 */
    .get_mctrl    = my_uart_get_mctrl,   /* 获取调制解调器控制信号 */
    .stop_tx      = my_uart_stop_tx,     /* 停止发送 */
    .start_tx     = my_uart_start_tx,    /* 开始发送 */
    .stop_rx      = my_uart_stop_rx,     /* 停止接收 */
    .startup      = my_uart_startup,     /* 打开串口 */
    .shutdown     = my_uart_shutdown,    /* 关闭串口 */
    .set_termios  = my_uart_set_termios, /* 设置串口参数(波特率等) */
    .type         = my_uart_type,        /* 返回串口类型字符串 */
    .request_port = my_uart_request_port,/* 申请端口资源 */
    .config_port  = my_uart_config_port, /* 配置端口 */
};

/* 用户空间使用串口 */
// int fd = open("/dev/ttyS0", O_RDWR);
// struct termios tty;
// tcgetattr(fd, &tty);
// cfsetispeed(&tty, B115200);   /* 设置输入波特率 */
// cfsetospeed(&tty, B115200);   /* 设置输出波特率 */
// tcsetattr(fd, TCSANOW, &tty);
// write(fd, "Hello\n", 6);

2.3.3 SPI(串行外设接口)

SPI 是一种同步、全双工、主从式串行通信总线,由 Motorola 公司提出,广泛用于连接传感器、Flash、显示屏等外设。

SPI 信号线

复制代码
SPI 连接示意图(一主一从):

主设备(Master)              从设备(Slave)
┌──────────────┐             ┌──────────────┐
│    SCLK  ────┼─────────────┼──── SCLK     │  时钟(主→从)
│    MOSI  ────┼─────────────┼──── MOSI     │  主出从入(主→从)
│    MISO  ────┼─────────────┼──── MISO     │  主入从出(从→主)
│    CS0   ────┼─────────────┼──── CS       │  片选(低有效)
└──────────────┘             └──────────────┘

一主多从(每个从设备需要独立的 CS 信号):
主设备 CS0 ──→ 从设备 0
主设备 CS1 ──→ 从设备 1
主设备 CS2 ──→ 从设备 2

SPI 的 4 种工作模式

模式 CPOL CPHA 时钟空闲状态 数据采样时刻
Mode 0 0 0 低电平 上升沿
Mode 1 0 1 低电平 下降沿
Mode 2 1 0 高电平 下降沿
Mode 3 1 1 高电平 上升沿
  • CPOL(时钟极性):0 = 空闲时低电平,1 = 空闲时高电平
  • CPHA(时钟相位):0 = 第一个边沿采样,1 = 第二个边沿采样

Linux SPI 驱动框架

c 复制代码
/*
 * SPI 设备驱动示例(读写 SPI Flash W25Q64)
 */
#include <linux/spi/spi.h>

/* SPI 传输:发送命令并接收数据 */
static int w25q64_read_id(struct spi_device *spi, u8 *id)
{
    u8 tx_buf[4] = {0x9F, 0x00, 0x00, 0x00};  /* 读 JEDEC ID 命令 */
    u8 rx_buf[4] = {0};

    struct spi_transfer xfer = {
        .tx_buf = tx_buf,
        .rx_buf = rx_buf,
        .len    = 4,
    };

    struct spi_message msg;
    spi_message_init(&msg);
    spi_message_add_tail(&xfer, &msg);

    int ret = spi_sync(spi, &msg);
    if (ret < 0) return ret;

    /* rx_buf[1..3] 是 Manufacturer ID + Device ID */
    id[0] = rx_buf[1];  /* 0xEF(Winbond) */
    id[1] = rx_buf[2];  /* 0x40 */
    id[2] = rx_buf[3];  /* 0x17(64Mbit) */
    return 0;
}

/* SPI 驱动的 probe 函数 */
static int w25q64_probe(struct spi_device *spi)
{
    u8 id[3];

    /* 配置 SPI 参数 */
    spi->max_speed_hz = 50000000;   /* 50MHz */
    spi->mode         = SPI_MODE_0; /* CPOL=0, CPHA=0 */
    spi->bits_per_word = 8;
    spi_setup(spi);

    w25q64_read_id(spi, id);
    pr_info("W25Q64 ID: %02X %02X %02X\n", id[0], id[1], id[2]);
    return 0;
}

/* 设备树匹配表 */
static const struct of_device_id w25q64_of_match[] = {
    { .compatible = "winbond,w25q64" },
    {}
};

static struct spi_driver w25q64_driver = {
    .driver = {
        .name           = "w25q64",
        .of_match_table = w25q64_of_match,
    },
    .probe  = w25q64_probe,
    .remove = w25q64_remove,
};
module_spi_driver(w25q64_driver);

设备树中的 SPI 设备描述

dts 复制代码
/* 在设备树中描述 SPI 总线上的 W25Q64 Flash */
&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_spi1>;

    w25q64: flash@0 {
        compatible = "winbond,w25q64";
        reg = <0>;                      /* 片选号 CS0 */
        spi-max-frequency = <50000000>; /* 最大时钟频率 50MHz */
        spi-cpol;                       /* CPOL=1(可选) */
        spi-cpha;                       /* CPHA=1(可选) */
    };
};

2.3.4 I2C(集成电路总线)

I2C 是由 Philips(现 NXP)公司开发的两线制同步串行总线,只需 SDA(数据线)和 SCL(时钟线)即可连接多个设备。

I2C 总线特性

复制代码
I2C 总线连接示意图:

VCC
 │
 ├── 上拉电阻(4.7kΩ)── SDA ──┬──────────────┬──────────────┐
 │                              │              │              │
 └── 上拉电阻(4.7kΩ)── SCL ──┼──────────────┼──────────────┤
                                │              │              │
                           主设备          从设备0         从设备1
                          (Master)       (Addr:0x48)    (Addr:0x50)

特点:
  - 只需 2 根信号线(SDA + SCL)
  - 支持多主多从(通过仲裁机制)
  - 每个从设备有唯一的 7 位或 10 位地址
  - 标准模式:100kbps;快速模式:400kbps;高速模式:3.4Mbps
  - 开漏输出 + 上拉电阻(允许多设备共享总线)

I2C 通信时序

复制代码
I2C 写操作时序(主设备向从设备写1字节):

START  地址(7位)  W  ACK  数据(8位)  ACK  STOP
  S   A6..A0  0   A    D7..D0    A     P

START 条件:SCL 高电平时,SDA 从高变低
STOP  条件:SCL 高电平时,SDA 从低变高
ACK:从设备拉低 SDA 表示应答

Linux I2C 驱动框架

c 复制代码
/*
 * I2C 设备驱动示例(读取 LM75 温度传感器)
 * LM75 I2C 地址:0x48(A2A1A0 = 000)
 */
#include <linux/i2c.h>

#define LM75_TEMP_REG   0x00   /* 温度寄存器 */
#define LM75_CONF_REG   0x01   /* 配置寄存器 */

/* 读取温度值 */
static int lm75_read_temp(struct i2c_client *client, int *temp_mc)
{
    s16 raw;
    int ret;

    /* i2c_smbus_read_word_data:读取 16 位寄存器值 */
    ret = i2c_smbus_read_word_data(client, LM75_TEMP_REG);
    if (ret < 0) return ret;

    /* LM75 温度数据格式:高 9 位有效,分辨率 0.5°C */
    raw = (s16)swab16(ret);   /* 字节序转换 */
    raw >>= 7;                 /* 右移 7 位,取高 9 位 */

    /* 转换为毫摄氏度(m°C) */
    *temp_mc = (raw / 2) * 1000 + (raw & 1) * 500;
    return 0;
}

/* probe 函数:设备与驱动匹配时调用 */
static int lm75_probe(struct i2c_client *client,
                      const struct i2c_device_id *id)
{
    int temp_mc;

    /* 检查 I2C 适配器是否支持所需功能 */
    if (!i2c_check_functionality(client->adapter,
                                  I2C_FUNC_SMBUS_BYTE_DATA)) {
        dev_err(&client->dev, "I2C 功能不支持\n");
        return -ENODEV;
    }

    lm75_read_temp(client, &temp_mc);
    dev_info(&client->dev, "当前温度:%d.%d°C\n",
             temp_mc / 1000, abs(temp_mc % 1000) / 100);
    return 0;
}

/* I2C 设备 ID 表 */
static const struct i2c_device_id lm75_id[] = {
    { "lm75", 0 },
    {}
};
MODULE_DEVICE_TABLE(i2c, lm75_id);

/* 设备树匹配表 */
static const struct of_device_id lm75_of_match[] = {
    { .compatible = "national,lm75" },
    {}
};

static struct i2c_driver lm75_driver = {
    .driver = {
        .name           = "lm75",
        .of_match_table = lm75_of_match,
    },
    .probe    = lm75_probe,
    .remove   = lm75_remove,
    .id_table = lm75_id,
};
module_i2c_driver(lm75_driver);

设备树中的 I2C 设备描述

dts 复制代码
&i2c1 {
    clock-frequency = <400000>;   /* I2C 时钟频率 400kHz */
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    lm75: temperature-sensor@48 {
        compatible = "national,lm75";
        reg = <0x48>;             /* I2C 从设备地址 0x48 */
    };

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;             /* AT24C02 EEPROM,地址 0x50 */
        pagesize = <8>;
    };
};

2.3.5 SPI 与 I2C 的对比

对比项 SPI I2C
信号线数量 4 根(SCLK/MOSI/MISO/CS) 2 根(SCL/SDA)
通信方式 全双工 半双工
速度 快(可达数十 MHz) 较慢(最高 3.4MHz)
从设备寻址 独立 CS 引脚 7/10 位地址
硬件复杂度 较高(多从设备需多 CS) 低(总线共享)
典型应用 Flash、ADC、显示屏 传感器、EEPROM、RTC

2.3.6 USB(通用串行总线)

USB 是目前最广泛使用的外设连接总线,支持热插拔、即插即用。

USB 版本演进

版本 速度 连接器 典型应用
USB 1.1 12Mbps(全速)/ 1.5Mbps(低速) Type-A/B 键盘、鼠标
USB 2.0 480Mbps(高速) Type-A/B/Mini/Micro U盘、摄像头
USB 3.0 5Gbps(超速) Type-A/B/Micro-B 高速存储
USB 3.1 10Gbps Type-C 高速传输
USB 3.2 20Gbps Type-C 高速传输

USB 拓扑结构

复制代码
USB 拓扑(星形,以 Hub 为中心):

主机(Host)
    │
    ├── USB Hub(最多 7 层级联)
    │       ├── USB 键盘
    │       ├── USB 鼠标
    │       └── USB Hub
    │               ├── U 盘
    │               └── USB 摄像头
    └── USB 设备(直连)

Linux USB 驱动框架

Linux USB 驱动基于 USB Core 子系统,驱动开发者只需实现设备相关的操作,无需关心 USB 协议细节:

c 复制代码
#include <linux/usb.h>

/* USB 设备 ID 表(VID:PID) */
static const struct usb_device_id my_usb_table[] = {
    { USB_DEVICE(0x1234, 0x5678) },   /* VendorID=0x1234, ProductID=0x5678 */
    {}
};
MODULE_DEVICE_TABLE(usb, my_usb_table);

/* probe 函数:USB 设备插入时调用 */
static int my_usb_probe(struct usb_interface *intf,
                        const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);

    pr_info("USB 设备插入:VID=%04X, PID=%04X\n",
            le16_to_cpu(udev->descriptor.idVendor),
            le16_to_cpu(udev->descriptor.idProduct));

    /* 查找 Bulk IN/OUT 端点 */
    struct usb_host_interface *iface_desc = intf->cur_altsetting;
    for (int i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        struct usb_endpoint_descriptor *ep = &iface_desc->endpoint[i].desc;
        if (usb_endpoint_is_bulk_in(ep))
            pr_info("找到 Bulk IN 端点,地址 0x%02X\n", ep->bEndpointAddress);
        if (usb_endpoint_is_bulk_out(ep))
            pr_info("找到 Bulk OUT 端点,地址 0x%02X\n", ep->bEndpointAddress);
    }
    return 0;
}

/* disconnect 函数:USB 设备拔出时调用 */
static void my_usb_disconnect(struct usb_interface *intf)
{
    pr_info("USB 设备拔出\n");
}

static struct usb_driver my_usb_driver = {
    .name       = "my_usb_driver",
    .id_table   = my_usb_table,
    .probe      = my_usb_probe,
    .disconnect = my_usb_disconnect,
};
module_usb_driver(my_usb_driver);

2.3.7 PCI/PCIe 总线

PCI(Peripheral Component Interconnect)及其升级版 PCIe(PCI Express)是 PC 和服务器中最重要的高速外设总线。

PCIe 特性

复制代码
PCIe 通道(Lane):
  - 每个 Lane 包含 1 对差分发送线 + 1 对差分接收线(全双工)
  - PCIe 1.0:每 Lane 2.5Gbps(单向)
  - PCIe 3.0:每 Lane 8Gbps(单向)
  - PCIe 4.0:每 Lane 16Gbps(单向)

常见配置:
  x1(1个Lane):网卡、声卡
  x4(4个Lane):NVMe SSD
  x8(8个Lane):RAID 卡
  x16(16个Lane):显卡

Linux PCI 驱动框架

c 复制代码
#include <linux/pci.h>

static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x8086, 0x100E) },   /* Intel 82540EM 网卡 */
    {}
};

static int my_pci_probe(struct pci_dev *pdev,
                        const struct pci_device_id *id)
{
    int ret;

    /* 1. 使能 PCI 设备 */
    ret = pci_enable_device(pdev);
    if (ret) return ret;

    /* 2. 申请 PCI 内存区域(BAR0) */
    ret = pci_request_regions(pdev, "my_pci_driver");
    if (ret) goto err_disable;

    /* 3. 映射 BAR0 到内核虚拟地址 */
    void __iomem *bar0 = pci_iomap(pdev, 0, 0);
    if (!bar0) { ret = -ENOMEM; goto err_release; }

    /* 4. 设置 DMA 掩码(支持 32 位 DMA 地址) */
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
    if (ret) goto err_unmap;

    pr_info("PCI 设备初始化成功,BAR0 = %p\n", bar0);
    return 0;

err_unmap:
    pci_iounmap(pdev, bar0);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

static struct pci_driver my_pci_driver = {
    .name     = "my_pci_driver",
    .id_table = my_pci_ids,
    .probe    = my_pci_probe,
    .remove   = my_pci_remove,
};
module_pci_driver(my_pci_driver);

2.3.8 其他常用接口

GPIO(通用输入输出)

GPIO 是最简单的数字接口,每个引脚可以独立配置为输入或输出:

c 复制代码
/* Linux GPIO 子系统 API */
#include <linux/gpio.h>

#define LED_GPIO    23
#define BUTTON_GPIO 24

/* 申请并配置 GPIO */
gpio_request(LED_GPIO, "led");
gpio_direction_output(LED_GPIO, 0);   /* 输出,初始低电平 */

gpio_request(BUTTON_GPIO, "button");
gpio_direction_input(BUTTON_GPIO);    /* 输入 */

/* 读写 GPIO */
gpio_set_value(LED_GPIO, 1);          /* 输出高电平 */
int val = gpio_get_value(BUTTON_GPIO); /* 读取输入电平 */

/* 释放 GPIO */
gpio_free(LED_GPIO);
gpio_free(BUTTON_GPIO);
CAN 总线

CAN(Controller Area Network)总线广泛用于汽车电子和工业控制,具有多主、差分信号、强错误检测等特点:

复制代码
CAN 总线特性:
  - 差分信号(CAN_H 和 CAN_L),抗干扰能力强
  - 多主总线,任何节点都可以发起通信
  - 基于消息 ID 的优先级仲裁
  - 速率:标准 CAN 最高 1Mbps;CAN FD 最高 8Mbps
  - 最大节点数:理论上无限制(实际受总线负载限制)

2.4 CPLD 与 FPGA

2.4.1 CPLD 与 FPGA 的概念

CPLD(Complex Programmable Logic Device,复杂可编程逻辑器件)和 FPGA(Field Programmable Gate Array,现场可编程门阵列)是两种可编程逻辑器件,在嵌入式系统中常用于扩展接口、实现定制逻辑、加速特定算法

复制代码
可编程逻辑器件的发展:
SPLD(简单 PLD)→ CPLD(复杂 PLD)→ FPGA(现场可编程门阵列)
  (数百门)        (数千门)           (数百万门)

2.4.2 CPLD 的特点与应用

CPLD 的结构

复制代码
CPLD 内部结构:
┌─────────────────────────────────────────┐
│                  CPLD                    │
│  ┌──────────┐  ┌──────────┐             │
│  │  宏单元0  │  │  宏单元1  │  ...        │
│  │(与或阵列)│  │(与或阵列)│             │
│  └──────────┘  └──────────┘             │
│  ┌─────────────────────────────────┐    │
│  │        可编程互连矩阵(PIM)      │    │
│  └─────────────────────────────────┘    │
│  ┌──────────────────────────────────┐   │
│  │         I/O 控制块               │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘

CPLD 的特点

  • 基于**乘积项(Product Term)**结构

  • 非易失性(断电后配置保留,上电即可工作)

  • 逻辑规模较小(数千~数万门)

  • 时序可预测(固定传播延迟)

  • 适合实现组合逻辑、状态机、地址译码

    CPLD 在嵌入式系统中的典型应用

    应用场景一:地址译码
    CPU 地址总线 A[23:0] → CPLD → 片选信号 CS0~CS7
    CPLD 根据地址范围自动生成对应外设的片选信号

    应用场景二:总线扩展
    CPU 8位数据总线 → CPLD → 16位外设数据总线
    CPLD 实现数据位宽转换

    应用场景三:时序控制
    CPU 读写信号 → CPLD → 满足外设时序要求的控制信号
    CPLD 插入等待周期,匹配慢速外设

CPLD 编程语言(VHDL 示例)

vhdl 复制代码
-- 地址译码器:将 CPU 地址映射到 4 个片选信号
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity addr_decoder is
    Port (
        addr  : in  STD_LOGIC_VECTOR(23 downto 20);  -- 高4位地址
        cs0   : out STD_LOGIC;   -- 0x000000~0x0FFFFF
        cs1   : out STD_LOGIC;   -- 0x100000~0x1FFFFF
        cs2   : out STD_LOGIC;   -- 0x200000~0x2FFFFF
        cs3   : out STD_LOGIC    -- 0x300000~0x3FFFFF
    );
end addr_decoder;

architecture Behavioral of addr_decoder is
begin
    cs0 <= '0' when addr = "0000" else '1';  -- 低有效
    cs1 <= '0' when addr = "0001" else '1';
    cs2 <= '0' when addr = "0010" else '1';
    cs3 <= '0' when addr = "0011" else '1';
end Behavioral;

2.4.3 FPGA 的特点与应用

FPGA 的内部结构

复制代码
FPGA 内部结构:
┌─────────────────────────────────────────────────────┐
│                       FPGA                           │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐         │
│  │ CLB│ │ CLB│ │ CLB│ │ CLB│ │ CLB│ │ CLB│  ...    │
│  └────┘ └────┘ └────┘ └────┘ └────┘ └────┘         │
│  CLB(可配置逻辑块)= LUT(查找表)+ 触发器 + MUX    │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │
│  │              可编程互连资源                    │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐           │
│  │  Block   │  │   DSP    │  │   PLL    │           │
│  │  RAM     │  │  Slice   │  │  /MMCM   │           │
│  └──────────┘  └──────────┘  └──────────┘           │
│  ┌──────────────────────────────────────────────┐   │
│  │              I/O 块(IOB)                    │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

FPGA 的特点

  • 基于 **LUT(查找表)**结构,可实现任意逻辑函数

  • 易失性(断电后配置丢失,需要外部配置 Flash)

  • 逻辑规模大(数万~数千万等效门)

  • 包含专用资源:Block RAM、DSP Slice、PLL、高速串行收发器

  • 适合实现复杂数字逻辑、协议加速、图像处理

    FPGA 在嵌入式系统中的典型应用

    应用场景一:协议桥接
    将 CPU 的 AXI 总线转换为自定义外设接口
    实现 MIPI CSI-2 → 并行像素数据的转换

    应用场景二:实时信号处理
    FIR/IIR 数字滤波器
    FFT 运算加速
    图像缩放、色彩空间转换

    应用场景三:多路 I/O 扩展
    同时控制数百路 GPIO
    实现 LED 矩阵扫描驱动
    多路 PWM 输出

    应用场景四:SoC 设计(Zynq 系列)
    Xilinx Zynq = ARM Cortex-A9 + FPGA
    ARM 运行 Linux,FPGA 实现硬件加速

Verilog 示例(PWM 控制器)

verilog 复制代码
// PWM 控制器:可调占空比的 PWM 输出
module pwm_controller (
    input  wire        clk,        // 系统时钟
    input  wire        rst_n,      // 复位(低有效)
    input  wire [7:0]  duty,       // 占空比(0~255,对应 0%~100%)
    output reg         pwm_out     // PWM 输出
);

reg [7:0] counter;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 8'd0;
        pwm_out <= 1'b0;
    end else begin
        counter <= counter + 1'b1;  // 8位计数器,自动溢出
        pwm_out <= (counter < duty) ? 1'b1 : 1'b0;
    end
end

endmodule
// 当 duty=128 时,占空比 = 128/256 = 50%
// 当 duty=64  时,占空比 = 64/256  = 25%

2.4.4 CPLD/FPGA 与 CPU 的接口

在嵌入式系统中,CPLD/FPGA 通常通过以下方式与 CPU 连接:

复制代码
连接方式一:并行总线(最常见)
CPU 地址总线 + 数据总线 + 控制信号 → CPLD/FPGA
CPU 像访问内存一样访问 FPGA 内部寄存器

连接方式二:SPI 接口
CPU SPI 主设备 → FPGA SPI 从设备
适合低速配置和数据传输

连接方式三:PCIe 接口(高性能场景)
CPU PCIe Root Complex → FPGA PCIe Endpoint
适合高带宽数据传输(如图像采集卡)

Linux 驱动访问 FPGA 寄存器(并行总线方式):
void __iomem *fpga_base = ioremap(FPGA_PHYS_BASE, FPGA_SIZE);

// 写 FPGA 寄存器
writel(0x12345678, fpga_base + FPGA_REG_CTRL);

// 读 FPGA 寄存器
u32 status = readl(fpga_base + FPGA_REG_STATUS);

2.5 原理图分析

2.5.1 原理图分析的重要性

宋宝华在书中强调:驱动工程师必须能够读懂硬件原理图。原理图是驱动开发的基础,通过分析原理图可以获得:

  1. 引脚连接关系:确定外设连接到 SoC 的哪些引脚
  2. 电气特性:电平标准(3.3V/1.8V)、上下拉电阻
  3. 地址信息:片选信号、地址线连接方式
  4. 中断连接:外设中断信号连接到 SoC 的哪个中断引脚
  5. 时钟连接:外设时钟来源

2.5.2 原理图分析方法

步骤一:确定核心芯片

首先找到主处理器(SoC)的原理图,了解其引脚功能定义:

复制代码
以 i.MX6ULL 为例,关键引脚功能:
UART1_TXD  → GPIO1_IO09(复用功能:UART1 发送)
UART1_RXD  → GPIO1_IO08(复用功能:UART1 接收)
I2C1_SCL   → GPIO1_IO02(复用功能:I2C1 时钟)
I2C1_SDA   → GPIO1_IO03(复用功能:I2C1 数据)
SPI1_CLK   → GPIO3_IO26(复用功能:SPI1 时钟)

步骤二:追踪信号连接

从 SoC 引脚出发,追踪信号线到外设芯片:

复制代码
原理图追踪示例(I2C 温度传感器):

i.MX6ULL                          LM75A
┌──────────────┐                 ┌──────────────┐
│ I2C1_SCL     ├─────────────────┤ SCL          │
│ (GPIO1_IO02) │                 │              │
│              │    4.7kΩ↑VCC   │              │
│ I2C1_SDA     ├─────────────────┤ SDA          │
│ (GPIO1_IO03) │                 │              │
│              │                 │ A0 ──── GND  │ ← 地址位 A0=0
│              │                 │ A1 ──── GND  │ ← 地址位 A1=0
│              │                 │ A2 ──── GND  │ ← 地址位 A2=0
│ GPIO1_IO05   ├─────────────────┤ OS/INT       │ ← 温度报警中断
└──────────────┘                 └──────────────┘
                                 I2C 地址 = 0x48(1001000b)

步骤三:提取驱动所需信息

从原理图中提取驱动开发所需的关键信息:

复制代码
从上述原理图提取的信息:
1. I2C 总线:I2C1
2. 设备地址:0x48(A2A1A0 = 000)
3. 中断引脚:GPIO1_IO05,连接到 LM75A 的 OS/INT 引脚
4. 中断触发方式:需查看 LM75A 数据手册(通常为低电平有效)

对应的设备树节点:
&i2c1 {
    lm75@48 {
        compatible = "national,lm75";
        reg = <0x48>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_LEVEL_LOW>;
    };
};

2.5.3 常见原理图符号与标注

复制代码
常见原理图标注说明:
VCC / VDD    ← 电源正极(VCC 通常指 5V,VDD 通常指 3.3V 或更低)
GND / VSS    ← 地(参考电位 0V)
NC           ← No Connect(悬空,不连接)
DNP / DNI    ← Do Not Populate(不焊接,预留位置)
R_PU         ← Pull-Up Resistor(上拉电阻)
R_PD         ← Pull-Down Resistor(下拉电阻)
C_BYPASS     ← 旁路电容(去耦电容)
TP           ← Test Point(测试点)

信号命名约定:
信号名后加 # 或 _N 或 _n  ← 低有效信号(如 CS#、RESET_N)
信号名后加 _P / _N         ← 差分信号正/负(如 USB_DP / USB_DN)

2.6 硬件时序分析

2.6.1 时序分析的基本概念

时序分析是驱动开发中的重要技能,驱动程序必须严格遵守硬件规定的时序要求,否则会导致数据读写错误。

关键时序参数

复制代码
时序参数定义:

         ┌─────────────────────────────────────────┐
信号 A   │                                         │
         ┘                                         └──
         ↑                                         ↑
         t_rise                                    t_fall

         ┌──────────────────────────────────────────────
信号 B   │
         ┘
         ↑
         t_setup(建立时间):信号 A 必须在信号 B 上升沿前稳定的时间
         t_hold(保持时间):信号 A 必须在信号 B 上升沿后保持稳定的时间

常用时序参数:
t_su(Setup Time)    ← 建立时间:数据在时钟有效沿前必须稳定的最短时间
t_h(Hold Time)      ← 保持时间:数据在时钟有效沿后必须保持稳定的最短时间
t_pd(Propagation Delay)← 传播延迟:信号从输入到输出的延迟时间
t_w(Pulse Width)    ← 脉冲宽度:信号保持高/低电平的最短时间
t_rec(Recovery Time)← 恢复时间:复位信号释放后到时钟有效的最短时间

2.6.2 SRAM 读时序分析

以 SRAM 读操作为例,分析典型的并行总线时序:

复制代码
SRAM 读时序图:

地址总线  ──┬─────────────────────────────────────────┬──
            │←────── t_ACS(地址建立到片选)──────────→│
片选 CS#  ──┘                                         └──
            │←──── t_CE(片选有效到数据有效)──────────→│
数据总线  ──────────────────────────────────────────────┬──
                                                        │
                                                   数据有效

关键时序参数(以 IS61WV102416 SRAM 为例):
t_AA(地址访问时间):10ns(从地址有效到数据有效)
t_CE(片选访问时间):10ns(从 CS# 有效到数据有效)
t_OE(输出使能访问时间):5ns(从 OE# 有效到数据有效)

驱动中处理时序的方法

c 复制代码
/*
 * 当 CPU 速度远快于外设时,需要插入等待周期(Wait State)
 * 或使用 ndelay()/udelay() 添加延时
 */

/* 方法一:通过 SoC 的 EIM(外部接口模块)配置等待周期 */
/* 在设备树中配置 EIM 时序参数 */
/*
&weim {
    ranges = <0 0 0x08000000 0x08000000>;
    fsl,weim-cs-gpr = <0x00610081>;

    sram@0,0 {
        compatible = "mtd-ram";
        reg = <0 0 0x800000>;
        fsl,weim-cs-timing = <
            0x0161030F  // CS 建立时间、保持时间
            0x00000000
            0x1C022000  // 读时序参数
            0x0000C000
            0x1404a38E  // 写时序参数
            0x00000000
        >;
    };
};
*/

/* 方法二:软件延时(适用于 GPIO 模拟总线) */
static void gpio_write_byte(u8 data)
{
    /* 设置数据 */
    set_data_bus(data);
    ndelay(10);          /* 等待数据稳定(10ns) */

    /* 拉低写使能 */
    gpio_set_value(WE_GPIO, 0);
    ndelay(20);          /* 保持写使能低电平(20ns) */

    /* 拉高写使能,数据被锁存 */
    gpio_set_value(WE_GPIO, 1);
    ndelay(10);          /* 写恢复时间 */
}

2.6.3 I2C 时序分析

I2C 总线有严格的时序要求,驱动程序(或硬件 I2C 控制器)必须满足这些要求:

复制代码
I2C 标准模式时序参数(100kbps):

SCL 时钟:
  t_HIGH(高电平时间):最小 4.0μs
  t_LOW(低电平时间):最小 4.7μs
  t_r(上升时间):最大 1000ns
  t_f(下降时间):最大 300ns

数据建立/保持:
  t_SU:DAT(数据建立时间):最小 250ns(在 SCL 上升沿前)
  t_HD:DAT(数据保持时间):最小 0ns(在 SCL 下降沿后)

START/STOP 条件:
  t_SU:STA(START 建立时间):最小 4.7μs
  t_HD:STA(START 保持时间):最小 4.0μs
  t_SU:STO(STOP 建立时间):最小 4.0μs
  t_BUF(总线空闲时间):最小 4.7μs

2.6.4 SPI 时序分析

复制代码
SPI Mode 0 时序(CPOL=0, CPHA=0):

CS#   ─┐                                              ┌─
       └──────────────────────────────────────────────┘
         ↑ t_CSS(CS 建立时间)

SCLK  ──┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐
         └───┘   └───┘   └───┘   └───┘   └───┘   └──
              ↑采样  ↑采样  ↑采样  ↑采样  ↑采样  ↑采样
              (上升沿采样 MISO)

MOSI  ──┬───────┬───────┬───────┬───────┬───────┬──
        │  D7   │  D6   │  D5   │  D4   │  ...  │
        └───────┴───────┴───────┴───────┴───────┴──
         ↑ 下降沿更新 MOSI

关键参数(以 W25Q64 为例,50MHz 时钟):
  t_CSS(CS 建立时间):最小 5ns
  t_CSH(CS 保持时间):最小 5ns
  t_CLKH(时钟高电平时间):最小 9ns
  t_CLKL(时钟低电平时间):最小 9ns

2.7 芯片手册阅读方法

2.7.1 芯片手册的类型

在驱动开发中,需要阅读多种类型的技术文档:

文档类型 英文名称 主要内容 使用场景
数据手册 Datasheet 引脚定义、电气特性、时序参数 了解芯片基本特性
参考手册 Reference Manual 寄存器详细描述、功能模块说明 编写驱动程序
应用笔记 Application Note 典型应用电路、软件示例 快速上手
勘误表 Errata 芯片已知 Bug 及规避方法 解决疑难问题
用户手册 User Manual 开发板使用说明 硬件调试

2.7.2 SoC 参考手册的阅读方法

SoC 参考手册通常有数千页,需要掌握高效的阅读方法:

第一步:查阅目录,定位目标模块

复制代码
i.MX6ULL 参考手册目录结构(部分):
Chapter 1:  Introduction
Chapter 8:  External Signals and Pin Multiplexing  ← 引脚复用配置
Chapter 28: General Purpose Input/Output (GPIO)    ← GPIO 寄存器
Chapter 55: Universal Asynchronous Receiver/Transmitter (UART) ← UART 寄存器
Chapter 31: I2C Controller                         ← I2C 寄存器
Chapter 52: Serial Peripheral Interface (SPI)      ← SPI 寄存器

第二步:阅读功能概述(Functional Description)

了解模块的工作原理、支持的功能和工作模式:

复制代码
以 UART 章节为例,功能概述包含:
  - 支持的波特率范围
  - 数据格式(数据位/校验位/停止位)
  - FIFO 深度(发送 FIFO 32字节,接收 FIFO 32字节)
  - 支持的中断类型(发送空、接收满、错误等)
  - DMA 支持
  - 流控制(RTS/CTS 硬件流控)

第三步:阅读寄存器描述(Register Description)

这是驱动开发最核心的部分,需要仔细阅读每个寄存器的位域定义:

复制代码
寄存器描述阅读示例(UART 控制寄存器 UCR1):

寄存器名称:UART Control Register 1 (UCR1)
地址偏移:0x80
复位值:0x00000000
访问类型:读/写

位域描述:
Bit 31-16:保留(读为0,写忽略)
Bit 15 (ADEN):自动波特率检测使能
              0 = 禁用
              1 = 使能
Bit 14 (ADBR):自动波特率检测
              0 = 未检测到
              1 = 检测到有效波特率
Bit 13 (TRDYEN):发送就绪中断使能
              0 = 禁用发送就绪中断
              1 = 使能发送就绪中断
Bit 12 (IDEN):空闲条件检测中断使能
...
Bit 0 (UARTEN):UART 使能
              0 = 禁用 UART
              1 = 使能 UART

驱动代码对应:
#define UCR1_UARTEN   BIT(0)   /* UART 使能位 */
#define UCR1_TRDYEN   BIT(13)  /* 发送就绪中断使能 */
#define UCR1_RRDYEN   BIT(9)   /* 接收就绪中断使能 */

/* 使能 UART */
u32 val = readl(uart_base + UCR1);
val |= UCR1_UARTEN;
writel(val, uart_base + UCR1);

第四步:阅读时序图(Timing Diagrams)

复制代码
时序图阅读要点:
1. 确认信号的有效电平(高有效/低有效)
2. 记录关键时序参数(建立时间、保持时间、访问时间)
3. 注意信号之间的先后顺序(哪个信号先有效)
4. 关注特殊状态(如总线竞争、高阻态)

第五步:查阅引脚复用配置(Pin Mux)

现代 SoC 的引脚通常具有多种复用功能,驱动开发前必须确认引脚配置:

复制代码
i.MX6ULL 引脚复用示例(UART1_TXD 引脚):

引脚名:UART1_TX_DATA
可选功能:
  ALT0:UART1_TX_DATA  ← 用作 UART1 发送
  ALT1:ENET1_RDATA03
  ALT2:I2C3_SCL
  ALT3:ENET2_RDATA01
  ALT4:CSI_DATA07
  ALT5:GPIO1_IO16     ← 用作普通 GPIO
  ALT6:SPDIF_OUT
  ALT8:ANATOP_OTG1_ID

驱动中通过设备树配置引脚复用:
pinctrl_uart1: uart1grp {
    fsl,pins = <
        MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX  0x1b0b1  /* TX */
        MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX  0x1b0b1  /* RX */
    >;
};

2.7.3 外设芯片数据手册的阅读方法

以读取 AT24C02 EEPROM 数据手册为例:

第一步:查看封装和引脚定义

复制代码
AT24C02 引脚定义(8引脚 DIP/SOIC 封装):
Pin 1 (A0):地址位 0(接 VCC 或 GND)
Pin 2 (A1):地址位 1
Pin 3 (A2):地址位 2
Pin 4 (GND):电源地
Pin 5 (SDA):I2C 数据线
Pin 6 (SCL):I2C 时钟线
Pin 7 (WP):写保护(接 GND 允许写入,接 VCC 禁止写入)
Pin 8 (VCC):电源(1.8V~5.5V)

I2C 设备地址:1010 A2 A1 A0(7位)
当 A2=A1=A0=0 时,地址 = 0x50

第二步:查看功能描述和操作时序

复制代码
AT24C02 写操作时序(字节写):

START → 设备地址(0x50) + W → ACK → 字节地址 → ACK → 数据 → ACK → STOP

写操作后需要等待内部写周期完成(tWR = 最大 5ms)
可通过轮询 ACK 来检测写操作是否完成(ACK Polling)

AT24C02 读操作时序(随机读):

START → 设备地址(0x50) + W → ACK → 字节地址 → ACK →
START → 设备地址(0x50) + R → ACK → 数据 → NACK → STOP

第三步:查看电气特性和时序参数

复制代码
AT24C02 关键电气参数(VCC = 5V):
  最大时钟频率:400kHz(快速模式)
  SDA/SCL 上升时间:最大 300ns
  SDA/SCL 下降时间:最大 300ns
  输入高电平:最小 0.7×VCC = 3.5V
  输入低电平:最大 0.3×VCC = 1.5V
  写周期时间(tWR):最大 5ms
  数据保持时间:最小 40 年(典型值)
  写入次数:最小 1,000,000 次

2.7.4 阅读芯片手册的注意事项

宋宝华在书中特别提醒驱动工程师注意以下几点:

① 注意寄存器的访问宽度

c 复制代码
/* 错误:用 8 位访问 32 位寄存器 */
writeb(0x01, base + REG_CTRL);   /* 可能导致其他位被清零 */

/* 正确:使用寄存器规定的访问宽度 */
writel(0x00000001, base + REG_CTRL);  /* 32 位寄存器用 writel */
writew(0x0001, base + REG_CTRL);      /* 16 位寄存器用 writew */
writeb(0x01, base + REG_CTRL);        /* 8  位寄存器用 writeb */

② 注意读清除(Read-Clear)寄存器

c 复制代码
/*
 * 某些状态寄存器的标志位是"读清除"的:
 * 读取该位后,硬件自动将其清零
 * 不能用"读-修改-写"的方式操作,否则会误清除其他标志位
 */

/* 错误:读-修改-写(会清除其他已置位的标志) */
u32 status = readl(base + INT_STATUS);
status &= ~TX_DONE_FLAG;   /* 试图只清除 TX_DONE,但读操作已清除所有标志 */
writel(status, base + INT_STATUS);

/* 正确:直接写 1 清除目标位(Write-1-to-Clear 类型) */
writel(TX_DONE_FLAG, base + INT_STATUS);  /* 只清除 TX_DONE 标志 */

③ 注意字节序(Endianness)

c 复制代码
/*
 * ARM 处理器默认小端(Little-Endian)
 * 某些外设寄存器或网络协议使用大端(Big-Endian)
 * 需要进行字节序转换
 */
#include <linux/byteorder/generic.h>

/* 主机字节序 → 大端(网络字节序) */
u16 net_val = cpu_to_be16(host_val);
u32 net_val = cpu_to_be32(host_val);

/* 大端 → 主机字节序 */
u16 host_val = be16_to_cpu(net_val);
u32 host_val = be32_to_cpu(net_val);

/* 主机字节序 → 小端 */
u16 le_val = cpu_to_le16(host_val);

④ 注意勘误表(Errata)

复制代码
勘误表阅读示例(假设某 SoC 的 I2C 模块存在 Bug):

Errata ID: ERR007805
模块:I2C
描述:在 400kHz 快速模式下,连续传输超过 256 字节时,
      第 257 字节可能发生数据错误。
影响版本:Rev 1.0, Rev 1.1
规避方法:将每次 I2C 传输限制在 256 字节以内,
          超过 256 字节时分多次传输,每次传输之间
          插入至少 1μs 的延时。

对应驱动代码修改:
#define I2C_MAX_TRANSFER  256   /* 受勘误表限制 */

if (len > I2C_MAX_TRANSFER) {
    /* 分批传输 */
    while (len > 0) {
        int chunk = min(len, I2C_MAX_TRANSFER);
        ret = i2c_transfer_chunk(client, buf, chunk);
        buf += chunk;
        len -= chunk;
        udelay(1);   /* 勘误表要求的延时 */
    }
}

本章小结

章节 核心知识点 驱动开发关联
2.1 处理器 ARM 体系结构、工作模式、MMU、Cache ioremap()、DMA 一致性、用户/内核空间切换
2.2 存储器 NOR/NAND Flash、DDR、地址映射 MTD 驱动、DMA 操作、ioremap()
2.3 接口与总线 UART/SPI/I2C/USB/PCIe/GPIO 各总线子系统驱动框架
2.4 CPLD 与 FPGA 可编程逻辑器件原理与应用 通过并行总线/SPI 访问 FPGA 寄存器
2.5 原理图分析 信号追踪、引脚连接、地址提取 设备树编写、寄存器地址确定
2.6 硬件时序分析 建立/保持时间、访问时间 等待周期配置、ndelay()/udelay()
2.7 芯片手册阅读 寄存器描述、时序图、勘误表 寄存器操作、字节序处理、Bug 规避

驱动工程师的硬件基础要求

复制代码
必须掌握:
  ✓ 能读懂 SoC 参考手册中的寄存器描述
  ✓ 能分析原理图,确定引脚连接和设备地址
  ✓ 理解 UART/SPI/I2C 的基本通信原理
  ✓ 了解 NOR/NAND Flash 的特性差异
  ✓ 理解 MMU 和虚拟地址/物理地址的关系

建议掌握:
  ✓ 能分析硬件时序图,配置正确的时序参数
  ✓ 了解 FPGA/CPLD 的基本工作原理
  ✓ 能使用示波器、逻辑分析仪验证硬件时序
  ✓ 了解 PCIe/USB 的基本协议结构

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年