第十章 系统时钟和定时器
10.1 系统时钟
通用时钟树模型
外部晶振 (12MHz/24MHz 唯一物理时钟源 )
│
▼
┌─────────────────┐
│ PLL (频率合成器) │ ← 核心!把低频晶振倍频到高频
│ • 输入: Fin │
│ • 输出: Fout = Fin × M / (P × 2^S) │
│ • 作用: 产生系统主频 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 时钟分频器(Divider) │ ← 按需分配不同频率
│ • FCLK: CPU 用 │
│ • HCLK: 总线用 │
│ • PCLK: 外设用 │
│ • 分频比可配置 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 时钟门控 (Gate) │ ← 省电关键!
│ • 不用的外设时钟关掉 │
│ • 动态功耗管理 │
└─────────────────┘
总线示意:
┌─────────┐ ┌─────────────────────────────┐ ┌─────────┐
│ CPU │◄────►│ AHB 高速总线 │◄────►│ 内存 │
│ (400MHz)│ │ (Advanced High-performance │ │(100MHz) │
└─────────┘ │ Bus) │ └─────────┘
│ • 32/64位宽(一次传更多数据) │
│ • 流水线传输 │
│ • 多主设备仲裁 │
└─────────────┬───────────────┘
│
┌─────────────┴───────────────┐
│ APB 桥接器 │ ← 速度转换 + 协议转换
│ (AHB-to-APB Bridge) │
└─────────────┬───────────────┘
│
┌─────────────┴───────────────┐
│ APB 低速总线 │
│ (Advanced Peripheral Bus) │
│ • 16/32位宽 │
│ • 简单协议,省电 │
│ • 适合慢速设备 │
└──────┬────────┬────────┬────┘
│ │ │
┌────┘ ┌────┘ ┌────┘
↓ ↓ ↓
┌──────┐ ┌──────┐ ┌──────┐
│ UART │ │ Timer│ │ GPIO│
│(50MHz)│ │(50MHz)│ │(50MHz)│
└──────┘ └──────┘ └──────┘
📌 一句话:时钟树就是「频率分配网络」,只需要知道「我要给某个外设多少频率」,具体路由由硬件/框架完成。
9.2 定时器和PWM
核心思想:时钟树决定「频率从哪来」,定时器决定「时间怎么数」,PWM决定「波形怎么出」。
9.2.1 定时器(Timer)
┌─────────────────────────────────────────┐
│ 定时器核心流水线(5步) │
├─────────────────────────────────────────┤
│ ① 时钟源选择 │
│ • 通常来自PCLK或独立低速晶振 │
│ • 现代芯片支持多路时钟源动态切换 │
│ │
│ ② 预分频 (Prescaler) │
│ • 8/16位寄存器,把高频时钟降下来 │
│ • 公式: f_pre = f_in / (prescale+1) │
│ │
│ ③ 主分频 (Divider) │
│ • 固定分频:2/4/8/16倍 │
│ • 公式: f_timer = f_pre / divider │
│ │
│ ④ 计数 + 比较 (Counter + Compare) │
│ • TCNT: 递减计数器,每个时钟周期 -1 │
│ • TCMP: 比较值,相等时触发事件 │
│ • 事件: 输出翻转 / 产生中断 / 触发DMA │
│ │
│ ⑤ 自动重载 (Auto-reload) │
│ • 计数到0后,自动从重载寄存器恢复 │
│ • 实现周期性定时,无需软件干预 │
└─────────────────────────────────────────┘
计数 + 比较 (Counter + Compare)
┌─────────────────────────────────────────────────────────┐
│ 时钟输入(来自时钟树,如 100MHz) │
│ ↓ │
│ ┌─────────┐ │
│ │ 预分频器 │ ← 可选,降低计数频率 │
│ │ /N │ │
│ └────┬────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ Counter │ ← 核心:每个时钟周期 +1 │
│ │ 计数器 │ (通常 16/32/64 位,可上/下计数) │
│ │ 0→N │ │
│ └────┬────┘ │
│ │ │
│ ↓ 比较 │
│ ┌─────────┐ │
│ │ Compare │ ← 匹配值(软件写入) │
│ │ 寄存器 │ │
│ │ = M │ │
│ └────┬────┘ │
│ │ │
│ Counter == Compare? │
│ │ │
│ ┌────┴────┐ │
│ │ 是 → 触发事件: │
│ │ • 产生中断(IRQ) │
│ │ • 翻转 GPIO(PWM 输出) │
│ │ • 启动 ADC 采样 │
│ │ • 重置 Counter(周期性)或停止(单次) │
│ │ • 加载新的 Compare 值(链式触发) │
│ └─────────┘ │
└─────────────────────────────────────────────────────────┘
模式 A:单次触发(One-shot)
Counter: 0 → 1 → 2 → ... → 999 → 1000(Compare) → 触发 → 停止
↑_________________________________________|
10ms 后执行一次动作
应用:延时启动、超时检测、单次采样
模式 B:周期性(Periodic / Auto-reload)
Counter: 0 → ... → 999 → 1000(Compare) → 触发 → 自动重置为 0
↓ ↓
翻转 PWM 继续计数
触发中断 循环往复
┌────┐ ┌────┐ ┌────┐
│ │ │ │ │ │
──────┘ └────┘ └────┘ └────┘────
<─ 1ms ─><─ 1ms ─><─ 1ms ─>
应用:系统 tick、PWM 波形、定时采样
定时器工作频率 = 时钟源 / (预分频+1) / 主分频
定时周期(秒) = (预分频+1) × 主分频 × 计数值 / 时钟源频率
9.2.2 PWM定时器:用「数字开关」模拟「模拟信号」
PWM通用原理
┌─────────────────────────────────────────┐
│ PWM = Pulse Width Modulation(脉冲宽度调制)│
├─────────────────────────────────────────┤
│ 核心思想:固定周期,改变高电平时间 → 改变平均电压 │
│ │
│ 周期 (Period) = 高电平 + 低电平时间 │
│ 占空比 (Duty) = 高电平时间 / 周期 × 100% │
│ │
│ 输出波形示例(周期1ms,占空比30%): │
│ │
│ 高电平 ████████░░░░░░░░░░░░░░░░░░░░ 300μs │
│ 低电平 ░░░░░░░░░░███████████████████ 700μs │
│ ↑ ↑ │
│ 周期开始 周期结束│
│ │
│ 应用: │
│ • LED亮度调节(0~100%无级调光) │
│ • 电机速度控制(直流/步进/伺服) │
│ • 蜂鸣器音调(频率+占空比控制音量) │
│ • 电源管理(DC-DC控制器反馈) │
└─────────────────────────────────────────┘
PWM 的本质:双 Compare 机制
┌─────────────────────────────────────────────────────────┐
│ 双 Compare 架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ Counter: 0 ─────────────────────────────────────→ │
│ ↑ ↑ │
│ │ │ │
│ Compare A Compare B │
│ (Duty 点) (Period 点) │
│ │ │ │
│ ↓ ↓ │
│ 输出翻转 ↑ 输出翻转 ↓ + 重置 │
│ │
│ 波形生成过程: │
│ │
│ 时刻: 0 D P D P │
│ │<─────>|<──────>|<─────>|<──────>| │
│ │ │ │ │ │ │
│ Counter: 0───────D───────P───────D───────P─────── │
│ ↑ ↑ ↑ ↑ ↑ │
│ │ │ │ │ │ │
│ 输出: ┌───────┐ ┌───────┐ ┌─────── │
│ │ │ │ │ │ │
│ ────┘ └───────┘ └───────┘ │
│ ↑ ↑ ↑ ↑ │
│ └高电平─┘ └低电平─┘ │
│ │
│ 关键动作: │
│ • 0 时刻:Counter 启动,输出强制拉高(或拉低,可配置) │
│ • 到达 Compare A (D):输出翻转 → 变低 │
│ • 到达 Compare B (P):输出翻转 → 变高 + Counter 清零 │
│ • 循环往复 │
│ │
└─────────────────────────────────────────────────────────┘
- 分离"时间流逝"与"事件触发"
Counter:客观时间的度量(与软件无关)
Compare:主观关注的时刻(软件配置) - 硬件确定性 vs 软件灵活性
硬件保证:Compare 匹配在时钟周期级精度
软件负责:决定 Compare 值和响应动作
9.3 WATCHDOG 喂狗机制
Watchdog 的本质: 它也是一个倒数定时器。但它减到 0 时,不会触发普通中断,而是直接拉低芯片的 RESET 引脚,强制整个系统硬重启!
核心机制
┌─────────────────────────────────────────────────────────┐
│ Watchdog 架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ 独立时钟源(关键!) │
│ ↓ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Counter │ ← │ 预分频 │ ← │ OSC/RTC │ │
│ │ 递减计数 │ │ │ │ 独立晶振 │ │
│ │ N→0 │ └─────────┘ └─────────┘ │
│ └────┬────┘ │
│ │ │
│ ↓ 计数到 0 超时时间=计数器从「初始值」减到「0」的时间 │
│ ┌─────────┐ │
│ │ 超时? │── 是 ──→ 触发复位/中断 │
│ │ │ │
│ │ 喂狗? │── 是 ──→ 重载计数器,继续运行 │
│ │ (Reload)│ │
│ └─────────┘ │
│ │
│ 关键特性: │
│ • 独立时钟:主 CPU 死机时仍能计数 │
│ • 不可屏蔽:一旦启动,软件无法禁用(部分设计) │
│ • 窗口机制:喂狗太早或太晚都触发复位(高级设计) │
│ │
└─────────────────────────────────────────────────────────┘
两种工作模式
模式 1:超时复位(Reset Mode)⭐ 核心功能
┌─────────────────────────────────────────┐
│ 流程: │
│ ① 启动看门狗 │
│ ② 程序正常运行 → 定期喂狗 │
│ ③ 程序死机/跑飞 → 无法喂狗 │
│ ④ 计数器归零 → 硬件复位信号触发 │
│ ⑤ 系统重启 → 恢复正常运行 │
└─────────────────────────────────────────┘
✅ 应用场景:
• 无人值守设备(基站、监控)
• 安全关键系统(汽车刹车控制)
• 死机后必须自愈的场景
⚠️ 注意:
• 复位后系统会丢失当前数据(RAM 内容清空)
• 需配合日志记录复位原因(看门狗复位标志位)
模式 2:超时中断(Interrupt Mode)🔔 预警功能
┌─────────────────────────────────────────┐
│ 流程: │
│ ① 计数器归零前 → 先触发中断 │
│ ② 中断服务程序尝试恢复系统 │
│ • 打印错误日志 │
│ • 重启关键任务 │
│ • 上报云端 │
│ ③ 如果恢复失败 → 继续计数 → 触发复位 │
└─────────────────────────────────────────┘
✅ 应用场景:
• 需要保存现场数据的场景
• 尝试软件自愈,避免硬重启
• 调试阶段定位死机原因
💡 现代实现:
• 两级看门狗:第一级中断警告,第二级复位
• Linux 内核:watchdog_ping() 失败后先 panic() 再重启
模式对比表
| 特性 | 超时复位 | 超时中断 |
|---|---|---|
| 触发条件 | 计数器归零 | 计数器归零(或预警告值) |
| 动作 | 硬件复位信号 | 触发 IRQ 中断 |
| 可恢复性 | ❌ 不可恢复(重启) | ✅ 可尝试软件恢复 |
| 用途 | 最终保护 | 预警/调试/日志 |
| 配置位 | WTCON[0] (Reset Enable) | WTCON[1] (Interrupt Enable) |
第十一章 通用异步收发器 UART (Universal Asynchronous Receiver/Transmitter)
并行数据(Parallel) (老技术,逐渐淘汰)
- 多根线同时传多个位
- CPU与内存总线(内部短距离仍用并行)
串行数据(Serial) (现代主流)
- 一根线一位一位传
- USB(通用串行总线)
- SATA(串口硬盘,取代IDE)
- PCIe(串口扩展总线,取代PCI)
- 以太网(网线)
- I2C、SPI、UART、CAN(嵌入式常用)
- HDMI、DisplayPort(视频接口)
它用来传输串行数据:
- 发送数据时,CPU将并行数据写入UART,UART按照一定的格式在一根电 线上串行发出;
- 接收数据时,UART检测另一根电线上的信号,将串行收集放在缓冲区中, CPU即可读取UART获得这些数据。
11.1、 UART 协议物理层:没有“时钟线”的默契
在数字电路中,通常需要一根“时钟线(Clock)”来告诉对方什么时候读数据(比如 I2C 和 SPI)。但 UART 只有两根线:TX(发) 和 RX(收) 。UART 之所以叫“异步(Asynchronous)”,是因为发送方和接收方之间没有共同的时钟线。 为了能对上节拍,双方必须提前约定好“潜规则(波特率)”,这就是所谓的 8N1 标准(最常用):
- 空闲状态 (Idle):高电平 (1) 为什么不设为 0?因为高电平代表电路连通。如果线断了,电平掉到 0,接收方能立刻感应到异常
- 起始位 (Start Bit): 逻辑
0。当 TX 引脚从 1 掉到 0 时,接收方的时钟立刻被唤醒,开始按照预定的波特率倒计时采样。 - 数据位 (Data Bits): 通常是 8 位(正好一个字节)。
- 校验位 (Parity Bit): 选配(N 代表 None,即不使用)。用于检查传输是否出错。
- 停止位 (Stop Bit): 逻辑
1。代表一个字节传输结束。
波特率 (Baud Rate): 每秒传输的位数。常见的有 115200 或 9600。
### 物理连接(最小系统)
CPU (TX) ────────────────> 设备 (RX)
CPU (RX) <──────────────── 设备 (TX)
CPU (GND) ─────────────── 设备 (GND)
⚠️ 关键:TX 接 RX,RX 接 TX(交叉连接),共地(GND)必须连通。
物理电平:TTL vs RS-232
| 标准 | 逻辑 1 | 逻辑 0 | 用途 |
|---|---|---|---|
| TTL | 3.3V/5V | 0V | 芯片间通信(CPU 到模块) |
| RS232 | -3~-12V | +3~+12V | PC 串口(需要电平转换芯片) |
| USB | 差分信号 | 差分信号 | 现代 PC 主流(需 USB 转串口模块) |
💡 工程经验:开发板通常是 TTL 电平,连接电脑需要 USB 转 TTL 模块(如 CH340/CP2102),直接接 RS232 会烧芯片
11.2 UART 控制器架构
┌─────────────────────────────────────────┐
│ UART 控制器内部结构 │
├─────────────────────────────────────────┤
│ │
│ 发送方向 (TX): │
│ CPU → 发送 FIFO → 发送移位器 → TX 引脚 │
│ │
│ 接收方向 (RX): │
│ RX 引脚 → 接收移位器 → 接收 FIFO → CPU │
│ │
│ 关键组件: │
│ • 波特率发生器:根据系统时钟分频产生采样时钟│
│ • FIFO 缓冲区:缓解 CPU 压力,减少中断次数 │
│ • 控制逻辑:中断/DMA/流控管理 │
└─────────────────────────────────────────┘
### 1.发送流程 (TX)
1. CPU 检查发送 FIFO 是否满
• 满:等待(查询)或 停止写入(中断/DMA)
• 空:继续
2. CPU 写入数据到发送保持寄存器(THR/FIFO)
3. 硬件自动处理:
• 添加起始位、停止位、校验位
• 移位器串行输出到 TX 引脚
4. 发送完成中断(可选):
• FIFO 空或移位器空时触发,通知 CPU 发下一个
### 2. 接收流程 (RX)
1. 硬件检测 RX 引脚起始位
2. 按波特率采样,串行转并行
3. 存入接收 FIFO
4. 接收中断触发:
• FIFO 数据达到阈值 或 超时
• CPU 读取数据寄存器(RDR/FIFO)
11.3 三种工作模式对比
1. 查询模式 (Polling)
- 原理:CPU 循环检查状态寄存器(
while(!TX_READY)) - 优点:简单,无需中断配置
- 缺点:占用 CPU 100% ,无法处理其他任务
- 适用:早期裸机、极简系统、调试打印(
printf底层)
2. 中断模式 (Interrupt) ⭐ 主流
- 原理:数据准备好后触发中断,CPU 在中断里读写
- 优点:CPU 利用率高,实时性好
- 缺点:大量数据时中断频繁,上下文切换开销大
- 适用:交互式命令、中等数据量、Linux 控制台
3. DMA 模式 (Direct Memory Access)
- 原理:DMA 控制器直接在内存和 UART FIFO 间搬运数据
- 优点:CPU 完全不参与,适合大数据流
- 缺点:配置复杂,延迟略高
- 适用:高速日志、文件传输、GPS 数据流
补 总线
一、分类
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的地位)
| 总线 | 线数 | 方向 | 特点 | 典型应用 |
|---|---|---|---|---|
| UART | 2(TX/RX) | 全双工 | 点对点,异步 | 调试串口、GPS模块 |
| I2C | 2(SDA/SCL) | 半双工 | 多主多从,地址寻址 | RTC、EEPROM、传感器 |
| SPI | 4(MOSI/MISO/SCK/CS) | 全双工 | 一主多从,片选控制 | Flash、显示屏、SD卡 |
| CAN | 2 | 半双工 | 工业级,抗干扰强 | 汽车电子 |
| USB | 2(D+/D-) | 半双工 | 热插拔,拓扑复杂 | 通用外设 |
| PCIe | 4(差分对) | 全双工 | 高速串行,现代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的地位)
| 总线 | 线数 | 方向 | 特点 | 典型应用 |
|---|---|---|---|---|
| UART | 2(TX/RX) | 全双工 | 点对点,异步 | 调试串口、GPS模块 |
| I2C | 2(SDA/SCL) | 半双工 | 多主多从,地址寻址 | RTC、EEPROM、传感器 |
| SPI | 4(MOSI/MISO/SCK/CS) | 全双工 | 一主多从,片选控制 | Flash、显示屏、SD卡 |
| CAN | 2 | 半双工 | 工业级,抗干扰强 | 汽车电子 |
| USB | 2(D+/D-) | 半双工 | 热插拔,拓扑复杂 | 通用外设 |
| PCIe | 4(差分对) | 全双工 | 高速串行,现代PC | 显卡、SSD |
一、 物理层:极简的两根线
I²C 最伟大的地方在于它只用两根线就能连接多达 127 个设备,特点如下:
- SCL (Serial Clock): 串行时钟线。由“主人”(Master,通常是 CPU)控制,决定说话的节奏。
- SDA (Serial Data): 双向串行数据线。所有信息(地址、数据、应答)都在这根线上跑。
- 每个连接到总线的器件都可以使用软件根据它的惟一的地址来识别。
- IIC的一个优点是它支持多主控(multimastering) , 其中任何一个能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
- 支持不同速率的通讯速度,标准速度(最高速度100kHZ),快速(最高400kHZ)
- SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰,上拉电阻只接在总线上。
- 为了避免总线信号的混乱,**要求各设备连接到总线的输出端时必须是 开漏输出
二、 协议层:严格的“点名制” (I2C 总线的信号类型)
I2C 总线在传送数据过程中共有3种类型信号:开始信号、结束信号和响应信号。
- (1)开始信号(S):SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
- (2)结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
- (3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA 电平。
它们的波形如图12.2、12.3所示。
数据位传输规则
SDA上传输的数据必须在SCL为高电平期间保持稳定, SDA上的数据只能在SCL为低 电平期间变化,如图12.4所示。
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 章的例子,整个启动过程是这样的:
-
链接脚本规划:
- 用
.设置 VMA(告诉 CPU 去哪跑)。 - 用
AT()设置 LMA(告诉烧录器存哪)。 - 用
__start/__end定义 符号(告诉 C 代码段在哪)。 - 把代码分好 .text/.data/.bss 段。
- 用
-
烧录:
- 程序被存到 NAND Flash 的 LMA 地址。
-
上电启动(C 代码工作) :
- CPU 从 Flash 开始执行。
- 利用 符号 找到 .data 段的源头(LMA)和目的地(VMA)。
- 拷贝 .data 段到 RAM。
- 清零 .bss 段。
- 跳转到 main() 函数(此时已经在 RAM 里跑了)。