📑 本页目录
计算机系统 · 全书复习 · 第一讲
把八章知识
接成一条完整的线从一条程序,到一盏LED
知道每章讲过什么,还要知道它们怎样合作。跟着“每500ms翻转一次LED”,把变量、指令、存储、总线、寄存器与电路逐个接起来。
前八章整理完成后的第一轮跨章复习。集中解决对象混淆、访问链路、计时与中断,不增加原书章节编号。原书全部内容与习题仍在各章讲义中。
C源文件 → 链接映像 → 复位启动 → 指令与总线 → GPIO → 电路与时间
先从八章的分工开始 →全书复习 · 第一讲 · 第01节
先把八章放到同一张图上
掌握要求:能说清每一章解决什么问题,再判断题目需要哪些章一起用。
前八章已经讲完。这一讲开始做跨章复习:跟着一条“让LED闪烁”的程序,把源码、存储、指令、总线、时钟、GPIO和中断接起来。 先把这条线讲通,后面再集中做存储、寻址与通信综合题。
| 原章 | 它解决什么问题 | 做题必须留下的能力 |
|---|---|---|
| 第一、二章 | 计算机由什么组成,信息怎样表示,指令怎样执行 | 认清CPU、存储、I/O;会算编码、容量、地址和基本执行过程 |
| 第三章 | 数据放在哪里,容量/速度怎样兼顾,怎样找到它 | 容量与地址范围、片选、存储层次;理解Cache和虚拟存储各自作用 |
| 第四章 | CPU与外设怎样交换数据,谁在何时占用通路 | 总线事务、地址译码、轮询/中断/DMA的区别 |
| 第五章 | ARM程序能看到哪些寄存器、状态和异常机制 | 区分通用寄存器、SP/LR/PC、状态寄存器与外设寄存器 |
| 第六章 | 每条ARM指令具体做了什么 | 寻址、数据宽度、条件、移位、算术与访存效果 |
| 第七章 | 怎样把指令组织成能正确合作的程序 | 分支/循环、子程序、栈、接口约定、C与汇编协作 |
| 第八章 | 怎样让这些能力控制实际芯片与电路 | 型号、引脚、时钟、配置顺序、状态/中断及边界条件 |
重要程度分三层。 第一层要能独立计算和写出来:位操作、地址/宽度、时钟/定时、分支/循环、栈和外设配置。第二层要能解释因果:为何中断能打断、为何需要启动初始化、为何DMA仍需CPU配置。第三层认识用途再查表:旧工具名、冷门模式、特定芯片的完整外设清单。
这里再讲背景,是为了让你能判断题目用了哪个模型。比如“有Cache的通用计算机”和“本讲的F103裸机”条件不同,不能把一种平台的假设强行放进另一种平台。
全书复习 · 第一讲 · 第02节
先纠正对象:CPU寄存器、RAM变量和外设寄存器
掌握要求:看到R0、一个C变量、GPIOA->BSRR时,能分别说出它们是什么。
CPU里的R0与GPIO外设里的ODR/BSRR都叫寄存器,但属于不同位置、不同访问方式。
| 对象 | 在哪里 | 怎样使用 | 典型题目 |
|---|---|---|---|
| R0—R12、SP、LR、PC | 处理器可见寄存器组 | 指令直接指定寄存器操作数 | 执行结果、参数传递、栈和返回 |
volatile uint32_t ticks |
通常位于SRAM,具体取决于存储布局 | 地址加读写指令;volatile限制相关优化 | 中断共享、变量初值、内存布局 |
| GPIO配置/数据寄存器 | MCU片上外设模块,映射到地址空间 | 对指定地址发起访问,硬件解释读写 | 配置引脚、读按键、控制输出 |
普通C变量也可能被优化到CPU寄存器、被常量折叠甚至消失,所以“写了一个变量就必然有一个固定RAM格子”并不成立。本讲的中断共享计数器明确用volatile,并依赖其32位对齐与目标平台访问特性。
做题先问:我在改哪个对象? MOVS R0,#1改R0;对SRAM地址做STR改相应内存;对BSRR地址做STR则向GPIO发出置位/复位命令。写指令同为STR,不代表目标的硬件语义相同。
全书复习 · 第一讲 · 第03节
从C源文件到Flash:编译、链接各负责什么
掌握要求:区分源文件、目标文件、链接映像、运行地址;知道初始化数据为什么可能有两份位置。
源文件先被编译/汇编成目标文件,再由链接器组合代码、常量、数据和符号,最后生成可下载映像。链接器结合存储布局决定各段和符号地址,下载工具把相应内容写进Flash等目标存储。
在典型MCU布局中:程序与常量主要在Flash;需要修改的数据主要在SRAM;有初值的可写数据可以在Flash保存初值副本,启动时复制到SRAM;零初始化数据按运行库规则清零。栈用于活动调用与局部存储,堆是否使用由程序和运行库决定。
uint32_t count = 7;
uint32_t total;
在常见链接布局中,count有Flash中的初值与SRAM中的运行位置;total的静态零初值由启动过程建立。Flash里存着初值,不等于CPU以后修改count也在改Flash。 不同工具和布局可能改变细节,题目给了段表就按段表判断。
编译器不会自动知道你的LED接哪一脚、哪个电平亮。它按代码和目标规则生成程序;引脚、时钟和电气仍由系统设计与初始化保证。这就是为什么第七章之后还必须学第八章。
全书复习 · 第一讲 · 第04节
从复位到main:第一条入口不是凭空出现的
掌握要求:能解释启动来源、向量表前两个字和Reset_Handler的职责。
F103硬件复位先使用HSI。BOOT引脚选择用户Flash、系统存储器或SRAM作为启动来源;启动代码随后可以按配置切换时钟。硬件默认时钟与main开始时的时钟,可能不同。
在Cortex-M的复位模型中,向量表首字用于初始MSP,第二字提供复位入口指针。以用户Flash映像位于0x08000000的典型布局为例,映像前两个字分别在0x08000000和0x08000004;复位时的启动映射还要结合BOOT理解。
例如第二字存0x08000101,表示Thumb入口,实际指令地址为0x08000100。最低位的状态标记与真正的指令地址要分清。不能把M系列向量表概括成“一排B指令”。
Reset_Handler和运行库建立必要环境,例如数据初始化、栈与系统初始化,然后进入main。具体执行顺序以匹配的启动模板为准。ENTRY、END、C的main与硬件复位向量,分属不同层次。
这一段考试常考过程与地址;实际调试时则用来区分“根本没进main”与“进了main但外设没配好”。不要求你无提示重写完整C运行库。
全书复习 · 第一讲 · 第05节
一次写GPIO,如何落到指令与总线事务
掌握要求:把C表达式拆成值、地址、访问宽度与硬件作用。
对本讲F103而言,GPIOA的BSRR地址为0x40010810。下面的语句向它写入bit24:
GPIOA->BSRR = 1u << (8u + 16u);
BSRR低16位写1对应置高,高16位写1对应复位到低;所以这次作用于PA8,将输出锁存值清为0。写入值为0x01000000。
用第六、七章的汇编表示,可以写成这样的教学示意:
LDR R0, =0x40010810 ; 地址:GPIOA_BSRR
LDR R1, =0x01000000 ; 命令:复位PA8
STR R1, [R0] ; 对该地址执行字写
这里LDR带等号是装载常量的伪指令写法;确切机器指令和常量池由汇编器处理。这不是承诺编译器必然把上面的C编成这三行。 它只把地址和值的职责展开给你看。
处理器发出地址与数据访问,片上总线互连/桥接/译码将它送到GPIO模块。地址决定选哪个目标,数据决定写什么,访问类型与宽度决定这次事务怎样进行。此处不需要操作系统系统调用。
本讲按F103裸机地址分析,不经过操作系统页表转换。第三章的Cache与虚拟存储仍要学:它们解释其他系统怎样处理速度与地址映射;其存在与细节由具体平台决定。
全书复习 · 第一讲 · 第06节
位操作要写成规则,别背一个神秘十六进制数
掌握要求:会计算BSRR命令、配置字段掩码,区分普通存储与命令寄存器。
对第p脚,BSRR置高写1u << p;复位到低写1u << (p + 16u)。它们是写命令,不是把一个变量原值读出来改一改。
如果目标是PA8的配置字段,PA8在CRH,字段为bit3:0。普通推挽、2MHz输出的4位编码为0010,可写成:
GPIOA->CRH = (GPIOA->CRH & ~0xFu) | 0x2u;
这是“清目标字段,再写新字段”,其他引脚配置保留。若题目改成PC6,则字段在CRL bit27:24;50MHz普通推挽编码0011,写入字段值为0x03000000,清位掩码为0x0F000000。
三种寄存器不能混成一个模板: 普通配置字段常用读改写;BSRR用直接命令写;EXTI_PR是写1清对应挂起位,也直接写目标掩码。对PR使用|=会把读到的其他1一并写回,从而可能误清别的来源。
原子地设置/复位GPIO目标位,不能自动证明整个算法并发正确;别处若仍然整字重写ODR,也需要统一访问约定。
全书复习 · 第一讲 · 第07节
从引脚电平到LED亮灭:电路补上最后一步
掌握要求:能根据接线判断有效电平;不把高电平与“亮”画等号。
本讲LED接法:3.3V → 510Ω限流电阻 → LED → PA8。GPIO输出低,电流从电源经电阻、LED流入PA8,LED亮;GPIO输出高时,通常没有足够正向压差,LED灭。
如果换成“PA8→电阻→LED→地”,有效电平会变成高。题目中的亮灭结果必须按电路判断,不能只看函数名SetBits。
推挽输出可以主动拉高或拉低;开漏写1表示释放引脚,需要外部上拉等条件才能建立高电平,输入读值还能被外部拉低。输入模式、输出模式、普通GPIO、复用功能也必须分清。
2MHz/10MHz/50MHz输出档描述相应驱动/边沿能力设置,不表示GPIO会自己周期输出该频率。周期波形需要程序翻转或定时器等硬件功能。限流电阻保证实际电流受控;MCU脚不能直接承担任意功率负载。
全书复习 · 第一讲 · 第08节
定时器先算时钟,再算寄存器
掌握要求:独立算出更新周期,并分清一次翻转与一次完整闪烁周期。
向上计数、边沿对齐、标准更新模型下:
fTIM是该定时器真正的输入时钟,不必等于SYSCLK,也不总等于PCLK。 F103在APB预分频为1时,相关TIM通常取PCLK;分频不为1时取2×PCLK。USART2不会因此也自动倍频。
例如SYSCLK=HCLK=72MHz,APB1除2:PCLK1=36MHz,TIM2=72MHz。若PSC=35999、ARR=999,则更新周期0.5秒。若每次更新翻转LED,亮0.5秒、灭0.5秒,完整周期1秒,频率1Hz。
本讲完整程序沿用第八章源码策略,主动切换HSI 8MHz,并设AHB/APB无分频。TIM2设PSC=7、ARR=999,得到1ms更新;主循环累计到500ms翻灯。因此程序里的PSC与上面72MHz直算例不同,结果仍一致。
PSC通常有缓冲,写入后还要按规则使新参数生效。初始化可以生成UG,再清掉初始化产生的UIF,最后使能中断和计数。不要刚开NVIC就把一次初始化更新误认成已经过了完整周期。
全书复习 · 第一讲 · 第09节
中断负责报时,主循环负责决定动作
掌握要求:能从一个时间轴解释ISR、共享计数与业务逻辑;知道volatile的边界。
定时器每1ms更新一次,置UIF;外设UIE和NVIC允许,且内核屏蔽/优先级条件满足时,进入TIM2_IRQHandler。ISR清本次源并给ticks加1,然后尽快返回。
从普通线程进入一个基本IRQ时,Cortex-M硬件基本帧保存R0—R3、R12、LR、PC、xPSR,共8个字;栈对齐可能另有填充,编译器也会根据ISR用到的寄存器生成必要的软件保护。不能把基本帧32字节当作所有ISR实际只消耗32字节栈。
主循环不断读取ticks,计算无符号差值是否达到500。它不用在ISR里等待500ms,也不用靠“执行若干条空指令”猜时间。
now = ticks;
if ((uint32_t)(now - since) >= 500u) {
since = now;
on ^= 1u;
GPIOA->BSRR = on ? (1u << 24) : (1u << 8);
}
无符号减法可在合理检查周期内处理计数回绕。例如since=0xFFFFFF00、now=0x000000F4,差值正好500。前提是仍能区分要测的时间间隔,不能隔了完整2³²ms甚至多圈后再猜经过了多久。
volatile使相关访问按目标语言/实现规则保留,不等于互斥锁,也不自动让count++成为不可打断的读改写。 本例只有ISR更新32位对齐ticks,主循环只做32位读取;若两边都更新复合状态,就要重新分析临界区。
全书复习 · 第一讲 · 第10节
完整应用源码:先说清构建条件,再看main
掌握要求:能按文件和时钟条件构建,修改周期,并解释每段代码服务于哪个环节。
下面给出一个完整应用源文件。目标固定STM32F103RCT6,采用匹配的官方CMSIS设备头、system文件和高密度启动模板,向量中TIM2入口需指向本文件的TIM2_IRQHandler;避免模板另有同名ISR。文件不捆绑厂商设备包。官方模板在进入main之前可能配置过时钟,本文件随后显式切回HSI 8MHz。
连接使用第07节低有效LED,仅PA8。应用采用HSI 8MHz、AHB/APB无分频,TIM2产生1ms节拍。HSI有容差,程序用于解释原理与练习配置,不承诺晶体级计时精度。
/* STM32F103RC; official CMSIS/device startup environment required. */
#include "stm32f10x.h"
#include <stdint.h>
static volatile uint32_t ticks;
static int clock_hsi_8mhz(void)
{
uint32_t guard = 1000000u;
RCC->CR |= 1u; /* HSION */
while (!(RCC->CR & 2u) && --guard) { }
if (!guard) return 0;
RCC->CFGR &= ~3u; /* SW = HSI */
guard = 1000000u;
while ((RCC->CFGR & 12u) && --guard) { }
if (!guard) return 0; /* wait for SWS = HSI */
RCC->CFGR &= ~((15u << 4) | (7u << 8) | (7u << 11));
RCC->CR &= ~(1u << 24); /* PLL no longer the source */
SystemCoreClock = 8000000u;
return 1;
}
static void led_init(void)
{
RCC->APB2ENR |= 1u << 2; /* GPIOA clock */
(void)RCC->APB2ENR; /* allow clock enable to propagate */
GPIOA->BSRR = 1u << 8; /* inactive high before output */
GPIOA->CRH = (GPIOA->CRH & ~15u) | 2u; /* PA8: PP, 2MHz */
}
static void tick_init(void)
{
RCC->APB1ENR |= 1u; /* TIM2 clock */
(void)RCC->APB1ENR;
TIM2->CR1 = 0;
TIM2->DIER = 0;
TIM2->PSC = 7;
TIM2->ARR = 999; /* 8MHz / 8 / 1000 = 1kHz */
TIM2->EGR = 1; /* UG loads the prescaler */
TIM2->SR = 0; /* discard initialization UIF */
TIM2->CNT = 0;
ticks = 0;
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_ClearPendingIRQ(TIM2_IRQn);
NVIC_EnableIRQ(TIM2_IRQn);
TIM2->DIER = 1; /* UIE */
TIM2->CR1 = 1; /* CEN */
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & 1u) {
TIM2->SR = (uint16_t)~1u; /* write 0 to clear UIF only */
++ticks;
}
}
int main(void)
{
uint32_t since, now;
unsigned on = 0;
if (!clock_hsi_8mhz()) for (;;) { } /* bounded startup failed */
led_init();
tick_init();
since = ticks;
for (;;) {
now = ticks;
if ((uint32_t)(now - since) >= 500u) {
since = now;
on ^= 1u;
GPIOA->BSRR = on ? (1u << 24) : (1u << 8);
}
}
}
改成亮1秒、灭1秒: 在保持1ms节拍的前提下,把主循环阈值500改1000,整周期变2秒、频率0.5Hz。改成1kHz PWM: 需要重新选择定时器输出通道、复用引脚、PSC/ARR/CCR和输出使能;仅把这个阈值改小,不能声称已经配置了硬件PWM。
程序用主循环观察节拍,因此实际翻转时刻还包括服务与主循环响应延迟。若主循环停顿很久,本例只翻转一次并从当前时刻重新计时;它不会追赶过去漏掉的所有翻转。严格边沿时序应考虑定时器硬件输出和相应误差分析。
全书复习 · 第一讲 · 第11节
出题人换条件时,你该改哪一层
掌握要求:面对变式先定位条件,不把前一题答案整段照搬。
| 题目变动 | 应先修改/重算的层 | 最常见的错法 |
|---|---|---|
| LED改成高有效 | 接线与输出逻辑 | 仍把BSRR复位当作灭 |
| PA8改PC6 | 端口时钟、CRL字段、输出掩码、接线 | 只换GPIOA为GPIOC,仍写第8位 |
| 8MHz改成72MHz | Flash/PLL/总线设置、fTIM与PSC/ARR | 只改SystemCoreClock变量 |
| 每500ms翻转改为500ms完整周期 | 事件与波形周期的关系 | 仍每500ms只翻转一次 |
| 按键能立即改变状态 | 输入上/下拉、EXTI来源、消抖、事件传递与状态机 | 在长延时后才读按键 |
| ISR和主循环共同改计数 | 并发约定、临界区与恢复旧屏蔽状态 | 只加volatile就认为没有竞争 |
| 改成DMA搬数据 | 外设/DMA配置、存储区、计数、触发与完成/错误处理 | 认为CPU从此完全不参与 |
| 给出带Cache/虚拟地址的另一平台 | 该平台存储与地址模型 | 直接套F103裸机地址结论 |
排错也按链路切开: 能否稳定复位和进main;时钟源/分频是否真生效;GPIO时钟和模式是否正确;BSRR值是否作用于正确脚;TIM的CNT是否推进、UIF是否出现;NVIC是否允许且向量正确;ticks是否变化;主循环是否满足条件;最后查实际引脚与LED接线。
这些检查能区分程序层、外设层与电路层,不需要先背几十个不相关函数名。观察寄存器时还要记住:有些状态通过读/写特定寄存器会被清除,调试器观察也可能产生影响,应查清语义。
全书复习 · 第一讲 · 第12节
先做这十二题,再看答案
掌握要求:能把本讲主线变成独立判断,而不是看到答案才觉得熟悉。
下面是本讲原创复习题,原书题仍在对应八章讲义中。建议先写下计算过程,再展开答案。
复习1.1 / 三个“寄存器”
R0、SRAM中的ticks、GPIOA_BSRR分别属于哪里?都能用MOVS R0,#1直接修改吗?
展开答案与判断理由
R0属于处理器寄存器组;ticks是按本例布局分配的SRAM对象;BSRR属于地址映射的GPIO外设。MOVS只改所指定的CPU寄存器,后两者需按地址访问,且外设另有语义。
复习1.2 / 初值放哪里
典型MCU布局中count=7的初值在Flash,运行时把count改成8,是改Flash还是SRAM?
展开答案与判断理由
修改运行位置,通常是SRAM。Flash可保存下载映像中的初值副本,启动时复制到SRAM;具体按链接布局判断。
复习1.3 / 复位向量
向量第二字为0x08000101,真正指令地址是什么?第一字有什么用途?
展开答案与判断理由
实际指令地址0x08000100,最低位指示Thumb入口;首字用于建立初始MSP。向量位置存入口值,与入口处的指令区分。
复习1.4 / BSRR命令
PA8低有效LED,向BSRR写0x01000000,LED亮还是灭?若改成高有效接法呢?
展开答案与判断理由
bit24复位PA8到低。按本讲低有效接法亮;高有效接法低则灭。结果必须结合电路,不能从“复位”一词猜。
复习1.5 / 字段掩码
PC6设50MHz普通推挽,要修改CRL还是CRH?清位掩码、写入值各是多少?
展开答案与判断理由
CRL bit27:24;清字段用~0x0F000000,字段新值0x03000000。最终(old & ~0x0F000000) | 0x03000000;另需GPIOC时钟和合适锁存初值。
复习1.6 / 总线与硬件语义
同样STR一字到SRAM地址与BSRR地址,为什么结果不能都理解成“保存一个变量”?
展开答案与判断理由
地址译码选中了不同目标。SRAM保存数据;BSRR写入由GPIO解释为置位/复位命令。访问宽度、权限、时钟与寄存器读写语义仍需符合目标规则。
复习1.7 / APB与TIM
HCLK72MHz,APB1除2,TIM2和USART2各是多少MHz?
展开答案与判断理由
PCLK1=36MHz;相关TIM在APB分频不为1时取2×PCLK,故TIM2=72MHz;USART2仍36MHz。
复习1.8 / 闪烁的二倍关系
TIM2时钟72MHz,PSC35999、ARR999,每更新翻转一次LED,更新周期、闪烁完整周期和频率是多少?
展开答案与判断理由
更新周期(36000×1000)/72000000=0.5s;两次翻转形成完整亮灭周期1s;闪烁频率1Hz。
复习1.9 / 1ms节拍
本讲8MHz程序为什么使用PSC7、ARR999?若不改其他条件,把500改成1000会怎样?
展开答案与判断理由
8MHz/(7+1)/(999+1)=1000Hz,节拍1ms。阈值1000表示每1s翻转,完整周期2s,频率0.5Hz。
复习1.10 / 回绕
since=0xFFFFFF00,now=0x000000F4,32位无符号差是多少?是否到500ms?
展开答案与判断理由
0x1F4=500;按本讲1ms计数且检查间隔满足范围条件,已经达到阈值。不能只做有符号比较或因now小于since就判错误。
复习1.11 / 中断与volatile
volatile能保证ISR与主循环同时执行count++时不丢更新吗?基本硬件异常帧32字节,是否代表ISR只用32字节栈?
展开答案与判断理由
都不能。count++是复合读改写,需要分析临界区/所有权;硬件基本帧之外还可能有对齐填充、软件保存、局部变量和嵌套等栈消耗。
复习1.12 / 定位故障
ticks不断增加、主循环每500ms写BSRR,但LED一直不亮,最先核对哪些条件?
展开答案与判断理由
核对写入作用于哪一脚/哪一电平,GPIO时钟与CRH模式、复用冲突、实际引脚电平、LED极性与接线/限流/供电。既然ticks已变化,不应先认定“中断根本没进”;仍需结合实际观测定位。
全书复习 · 第一讲 · 第13节
这一讲过关后,下一轮怎么学
掌握要求:知道已经掌握什么,知道下一轮该集中补什么。
本讲过关标准:能从“每500ms翻灯”追到时钟、寄存器、指令、总线和接线;给你换芯片时钟、引脚、有效电平或周期定义,知道该改哪里;看见SRAM变量、CPU寄存器、外设寄存器时不再混在一起。
下一轮集中做第三、六、七章的连接:容量与地址范围 → 小端字节 → LDR/STR宽度 → 基址/偏移/数组 → 栈与参数。 它们是算错地址、读错数、写错程序的共同来源。本讲先把系统主线跑通,不在同一讲再塞进所有Cache映射与页表计算。
需要回看的章节:第一、二章补信息表示和执行过程;第三章补存储;第四章补总线/中断/DMA;第五章补寄存器与异常;第六章补指令/寻址;第七章补程序/ABI;第八章补型号、时钟和外设。正文每一节都有相应的掌握要求,按错题返回即可。