《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 原理图分析的重要性
宋宝华在书中强调:驱动工程师必须能够读懂硬件原理图。原理图是驱动开发的基础,通过分析原理图可以获得:
- 引脚连接关系:确定外设连接到 SoC 的哪些引脚
- 电气特性:电平标准(3.3V/1.8V)、上下拉电阻
- 地址信息:片选信号、地址线连接方式
- 中断连接:外设中断信号连接到 SoC 的哪个中断引脚
- 时钟连接:外设时钟来源
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年