韦东山开发手册阅读笔记(四)

0 阅读12分钟

补 总线

一、分类

1. 按数据传输方式分

类型定义例子
并行总线多根数据线同时传输(8位、16位、32位)内存DDR、老式打印机接口
串行总线单根/少数几根线,逐位传输UART、I2C、SPI、USB、PCIe

2. 按使用场景分

层级位置典型总线速度
片内总线CPU芯片内部AHB、APB、AXI几百MHz~几GHz
片外总线芯片与外部设备之间I2C、SPI、UART、USB几Kbps~几Gbps

二、AHB/APB 与 I2C 的关系

完全不是一回事,但有关联

┌─────────────────────────────────────┐
│           CPU 芯片内部              │
│  ┌─────────┐    ┌─────────┐        │
│  │  CPU核  │◄──►│  内存   │  ← AHB总线(高速,并行)
│  └────┬────┘    └─────────┘        │
│       │                             │
│       ▼ AHB桥接                     │
│  ┌─────────┐    ┌─────────┐        │
│  │  外设1  │    │  外设2  │  ← APB总线(低速,并行)
│  │ (定时器)│    │ (GPIO)  │        │
│  └────┬────┘    └────┬────┘        │
│       │              │              │
│       ▼              ▼              │
│   I2C控制器 ──► SDA/SCL引脚 ────────┼──► 到芯片外部
│   (在APB上)                       │
└─────────────────────────────────────┘
              │
              ▼
        外部I2C设备(RTC、EEPROM、传感器等)
总线位置性质速度
AHB片内并行、高速100MHz+
APB片内并行、低速几十MHz
I2C片外串行、极低速100KHz~3.4MHz

关键理解

  • AHB/APB是芯片内部的并行总线,连接CPU和各种外设控制器
  • I2C控制器挂在APB总线上,CPU通过APB总线配置I2C控制器
  • I2C控制器再通过两根线(SDA/SCL)外部世界通信

三、串行总线家族(I2C的地位)

总线线数方向特点典型应用
UART2(TX/RX)全双工点对点,异步调试串口、GPS模块
I2C2(SDA/SCL)半双工多主多从,地址寻址RTC、EEPROM、传感器
SPI4(MOSI/MISO/SCK/CS)全双工一主多从,片选控制Flash、显示屏、SD卡
CAN2半双工工业级,抗干扰强汽车电子
USB2(D+/D-)半双工热插拔,拓扑复杂通用外设
PCIe4(差分对)全双工高速串行,现代PC显卡、SSD

I2C的独特优势:仅需2根线,支持128个设备,硬件最简单。

第十二章 I2C接口

I2C(Inter-Integrated Circuit,又称 IIC)总线是一种由 PHILIPS 公司开发的串行总线,用 于连接微控制器及其外围设备。

零、总线分类:串行 vs 并行,片内 vs 片外

0.1. 按数据传输方式分

类型定义例子
并行总线多根数据线同时传输(8位、16位、32位)内存DDR、老式打印机接口
串行总线单根/少数几根线,逐位传输UART、I2C、SPI、USB、PCIe

0.2. 按使用场景分

层级位置典型总线速度
片内总线CPU芯片内部AHB、APB、AXI几百MHz~几GHz
片外总线芯片与外部设备之间I2C、SPI、UART、USB几Kbps~几Gbps

关键理解

  • AHB/APB是芯片内部的并行总线,连接CPU和各种外设控制器
  • I2C控制器挂在APB总线上,CPU通过APB总线配置I2C控制器
  • I2C控制器再通过两根线(SDA/SCL)外部世界通信

0.3、串行总线家族(I2C的地位)

总线线数方向特点典型应用
UART2(TX/RX)全双工点对点,异步调试串口、GPS模块
I2C2(SDA/SCL)半双工多主多从,地址寻址RTC、EEPROM、传感器
SPI4(MOSI/MISO/SCK/CS)全双工一主多从,片选控制Flash、显示屏、SD卡
CAN2半双工工业级,抗干扰强汽车电子
USB2(D+/D-)半双工热插拔,拓扑复杂通用外设
PCIe4(差分对)全双工高速串行,现代PC显卡、SSD

一、 物理层:极简的两根线

I²C 最伟大的地方在于它只用两根线就能连接多达 127 个设备,特点如下:

  1. SCL (Serial Clock): 串行时钟线。由“主人”(Master,通常是 CPU)控制,决定说话的节奏。
  2. SDA (Serial Data): 双向串行数据线。所有信息(地址、数据、应答)都在这根线上跑。
  3. 每个连接到总线的器件都可以使用软件根据它的惟一的地址来识别。
  4. IIC的一个优点是它支持多主控(multimastering) , 其中任何一个能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
  5. 支持不同速率的通讯速度,标准速度(最高速度100kHZ),快速(最高400kHZ)
  6. SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰,上拉电阻只接在总线上
  7. 为了避免总线信号的混乱,**要求各设备连接到总线的输出端时必须是 开漏输出 image.png

image.png

二、 协议层:严格的“点名制” (I2C 总线的信号类型)

I2C 总线在传送数据过程中共有3种类型信号:开始信号、结束信号和响应信号。

  • (1)开始信号(S):SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
  • (2)结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
  • (3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA 电平。

它们的波形如图12.2、12.3所示。

image.png

数据位传输规则

SDA上传输的数据必须在SCL为高电平期间保持稳定, SDA上的数据只能在SCL为低 电平期间变化,如图12.4所示。

image.png

1. 主机写从机(最常见)
主机: [S] + [从机地址+写位0] + [ACK] + [寄存器地址] + [ACK] + [数据1] + [ACK] + ... + [数据N] + [ACK] + [P]
                ↓                        ↓                    ↓
            寻址从机                指定操作寄存器         写入数据

示例:向地址0x50的EEPROM写数据到寄存器0x10
[S] [0xA0] [ACK] [0x10] [ACK] [0x55] [ACK] [0xAA] [ACK] [P]
     ↑写位

 7位地址 vs 8位传输字节    
步骤1: 7位地址(二进制)
0x50 = 101 0000

步骤2: 左移1位(给R/W位腾出位置)
  101 0000  ← 原始7位地址
↓ 左移1位
 1010 0000  ← 现在bit0空出来了!

步骤3: 加上R/W位
写操作(R/W=0): 1010 0000 = 0xA0
读操作(R/W=1): 1010 0001 = 0xA1
R/W位:
• 0 = 写操作(主机→从机)
• 1 = 读操作(主机←从机)
2. 主机读从机(两步法)
步骤1: 写寄存器地址(定位读位置)
主机: [S] + [从机地址+写位0] + [ACK] + [寄存器地址] + [ACK] + [Sr]  ← 重复起始

步骤2: 读数据
主机: [从机地址+读位1] + [ACK] + [数据1] + [ACK] + ... + [数据N] + [NACK] + [P]
                              ↑主机收数据                         ↑最后一个发NACK

示例:从0x50的寄存器0x10读2字节
[S][0xA0][ACK][0x10][ACK][Sr][0xA1][ACK][D1][ACK][D2][NACK][P]
    写位↑                     读位↑
3. 关键细节
细节说明易错点
地址左移1位7位地址 + R/W位 = 8位传输字节0x50地址,写=0xA0,读=0xA1
重复起始(Sr)读写切换时不用发P,直接发Sr漏发Sr会导致总线释放,从机丢失状态
ACK时机主机发数据→从机ACK;从机发数据→主机ACK最后一个字节主机必须发NACK
时钟延展从机可拉低SCL暂停传输(忙时)主机必须支持等待,不能超时硬退出

三、开漏——实现"线与"逻辑:多设备共享总线不短路,支持仲裁和时钟同步

为什么I2C所有设备都必须用开漏?

原因1:实现"线与"——多设备共享同一根线

假设SDA总线上接了3个设备(主控+2个从机),都想控制SDA:

        VCC (3.3V)
          │
         [Rp]
          │
         SDA总线 ───┬────────┬────────┐
                   │        │        │
              ┌────┴────┐ ┌─┴─────┐ ┌─┴─────┐
              │ 主控    │ │ 设备1 │ │ 设备2 │
              │ SDA驱动 │ │SDA驱动│ │SDA驱动│
              │ [开漏]  │ │[开漏] │ │[开漏] │
              └────┬────┘ └───┬───┘ └───┬───┘
                   │          │         │
                  GND        GND       GND

场景:主控想发1,设备1想发0,设备2想发1

主控:  关闭NMOS(不拉低)─────┐
                             ├──► SDA被设备1拉低到0V
设备1: 导通NMOS(拉低)◄─────┘
设备2: 关闭NMOS(不拉低)─────┘

结果:SDA = 0(低电平)

这就是"线与":任一个设备拉低,总线就是低
所有设备都释放(不拉低),上拉电阻才把总线拉到高

如果用推挽会怎样?

主控(推挽)想发1:内部PMOS导通,SDA接到VCC
设备1(推挽)想发0:内部NMOS导通,SDA接到GND

        VCC ◄───┐
              [PMOS导通]
                │
               SDA ──── 短路!
                │
              [NMOS导通]
        GND ◄───┘

结果:VCC通过PMOS和NMOS直接短路到GND,大电流烧毁芯片!

原因2:支持多主机仲裁(谁控制总线)

I2C是多主机总线,可能两个CPU同时想发送数据:

CPU_A 想发送:1 0 1 1...
CPU_B 想发送:1 0 0 1...

时钟周期1:都发1,SDA=1 ✓
时钟周期2:都发0,SDA=0 ✓
时钟周期3:A发1,B发0 → SDA=0(因为B拉低了)

CPU_A检测到:我想发1,但总线是0 → 有人跟我争 → 我退出仲裁
CPU_B继续发送,赢得总线控制权

仲裁能工作的前提:所有主机都能"拉低"总线,但只能"被动释放"让总线变高。这就是开漏的特性。

如果用推挽,CPU_A强制输出1,CPU_B强制输出0,直接短路。

原因3:时钟同步(SCL的"线与")

多个主机可能同时驱动SCL时钟线:

主机A的时钟(较快):
        
    SCL_A    ─┐   ┌───┐   ┌───┐   ┌───┐   ┌─┐
              └───┘   └───┘   └───┘   └───┘
              
              ↑   ↑   ↑   ↑   ↑   ↑   ↑   ↑
              │   │   │   │   │   │   │   │
              1   2   3   4   5   6   7   8
              
主机B的时钟(较慢,低电平更长):

    SCL_B    ─┐       ┌───┐       ┌───┐       ┌─┐
              └───────┘   └───────┘   └───────┘
              
              ↑       ↑       ↑       ↑       ↑
              │       │       │       │       │
              1       2       3       4       5

实际SCL总线(开漏"线与"结果):

    SCL      ─┐       ┌───┐       ┌───┐       ┌─┐
    总线      └───────┘   └───────┘   └───────┘
              ↑       ↑       ↑       ↑       ↑
              │       │       │       │       │
              1       2       3       4       5
              
              ═══════════════════════════════════
              关键规律(与逻辑):
              ### ✅ 必须掌握

1.  **理解VMA vs LMA**:运行地址和加载地址的区别
1.  **段的概念**:.text/.data/.bss的作用
1.  **AT()语法**:如何指定加载地址
1.  **符号定义**:如何用链接脚本定义C代码可用的符号
              高电平 = 所有主机都释放(都高)→ 上拉电阻拉高
              低电平 = 任一主机拉低 → 总线就低
               
规律:SCL高电平时间 = 所有主机中最短的高电平
     SCL低电平时间 = 所有主机中最长的低电平

这样实现了时钟同步:慢速设备可以拉低SCL让快速设备等待
问题答案
上拉电阻接在哪里接在总线本身(SDA和SCL两根线),不在设备内部
谁用开漏所有设备(主控+所有外设)的I2C引脚,都必须是开漏
为什么必须开漏实现"线与"逻辑:多设备共享总线不短路,支持仲裁和时钟同步

四、链接脚本(Linker Script)

📖 一、链接脚本是什么?

链接脚本是告诉链接器(Linker)如何布局程序的「地图」

链接脚本是程序的「内存地图」,告诉链接器:代码和数据应该放在内存的哪个位置,哪些在Flash存储,哪些在RAM运行,各段如何排列。

源代码 (.c/.S)
    ↓ 编译
目标文件 (.o)  →  链接器  →  可执行文件 (.elf/.bin)
                   ↑
          链接脚本 (.lds)
          "告诉链接器:
           • 代码放哪里
           • 数据放哪里
           • 各段顺序如何"

核心作用

问题链接脚本的解决方案
代码应该从哪个地址开始执行?指定加载地址运行地址
多个.o文件如何组合?指定段(section)的排列顺序
全局变量放在哪?分配**.data/.bss段**的位置
栈和堆怎么安排?预留内存空间

二、核心段 (Sections)

  • .text (代码段): 存放写的函数、逻辑指令。它们是只读的。

  • .rodata (只读数据段): 存放 const 修饰的常量,比如 char *str = "Hello"; 里的字符串。

  • .data (数据段): 存放已初始化的全局变量。例如 int a = 10;

  • .bss (BSS 段): 存放未初始化的全局变量。例如 int b;。系统启动时,必须手动把这块区域“清零”,否则变量 b 的初始值就是乱码。 编译器会把你的代码和变量分成不同的「段」(Section),链接脚本决定把它们放在哪里。

📦 三个核心段
段名存放内容特点启动时需要做什么?
.text代码指令、常量只读,不能改不用管,直接跑
.data已初始化的全局变量 (如 int a = 100;)可读可写需要拷贝 (从 Flash 拷贝到 RAM)
.bss未初始化的全局变量 (如 int b;)可读可写,默认是 0需要清零 (在 RAM 里填 0)
❓ 为什么要分开?
  • .text 不用拷贝,因为 Flash 里也能读指令。
  • .data 必须拷贝到 RAM,因为变量运行时是要修改的,Flash 不能改。
  • .bss 不需要占 Flash 空间(反正都是 0),只需要在 RAM 里预留空间并清零即可,节省存储。

启动代码的任务就是:把 .data 从 Flash 搬到 RAM,把 .bss 清零。

三、加载地址 (LMA) vs 运行地址 (VMA)

  • 加载地址 (LMA - Load Memory Address): 程序“住”在哪?——程序编译好后,存放在 Flash/NAND 里的地址。(通常在 NAND Flash 里,掉电不丢失)。

  • 运行地址 (VMA - Virtual/Runtime Memory Address): 程序“在哪干活”?—— 程序真正被拷贝到 SDRAM/内存 里运行的地址。(通常在 SDRAM 里,速度快)。

为什么要分两个地址?

因为嵌入式系统通常从 Flash 启动,但为了跑得快,要把代码拷贝到 SDRAM 里运行。

  • 链接器需要知道:代码最终要在哪里跑?(VMA,用于生成跳转指令)
  • 烧录器需要知道:代码要存到 Flash 的哪个位置?(LMA,用于生成二进制文件)

四、如何串联起启动流程?

结合第 12 章的例子,整个启动过程是这样的:

  1. 链接脚本规划

    • . 设置 VMA(告诉 CPU 去哪跑)。
    • AT() 设置 LMA(告诉烧录器存哪)。
    • __start/__end 定义 符号(告诉 C 代码段在哪)。
    • 把代码分好 .text/.data/.bss 段。
  2. 烧录

    • 程序被存到 NAND Flash 的 LMA 地址。
  3. 上电启动(C 代码工作)

    • CPU 从 Flash 开始执行。
    • 利用 符号 找到 .data 段的源头(LMA)和目的地(VMA)。
    • 拷贝 .data 段到 RAM。
    • 清零 .bss 段。
    • 跳转到 main() 函数(此时已经在 RAM 里跑了)。