纯手写GPIO寄存器
以下是完整的单文件 main.c,所有代码和寄存器操作都在一个文件中,可直接编译运行:
代码特点说明
-
纯寄存器操作:所有外设访问均通过宏定义的寄存器地址直接操作,未使用任何库函数。
-
原子操作:LED 亮灭使用 BSRR 寄存器,避免了 ODR 读-改-写的中断风险。
-
可移植性:修改 LED_PIN 和对应的 RCC_APB2ENR 位即可更换引脚。
-
延时函数:使用简单的循环延时,未使用定时器(后续学习 TIM 后可替换为精确延时)。
-
注释完整:每行关键操作都有详细注释,便于理解和复习。
编译注意事项
-
需要包含 stm32f10x.h 或自行定义必要的类型(如 unsigned int 已在 C 标准中定义)
-
启动文件和链接脚本需根据具体开发板配置
-
如需使用外部晶振达到 72MHz,需额外配置 RCC 的 PLL 相关寄存器
EXTI+NVIC
这段代码实现的是外部中断控制 LED:按键接 PA0,LED 接 PC13。主函数配置完寄存器后只剩一句空转 while (1) { },从此不再过问按键——按下按键时硬件自己把 EXTI Line0 的挂起位置 1 并通知内核,CPU 跳进 EXTI0_IRQHandler 翻转一次 LED、清掉挂起标志,再返回空转。轮询代码一行都没有,按键的响应不依赖程序当时正在做什么。
配置顺序按信号的走法排:先开 AFIO/GPIOA/GPIOC 三个时钟,再把 PA0 设成上拉输入、PC13 设成推挽输出,然后用 AFIO 把 Line0 映射到 GPIOA,接着在 EXTI 侧选下降沿并解除屏蔽,最后在内核 NVIC 侧设优先级分组、排优先级、使能 6 号中断。前五步是在让信号进得来,最后一步是让 CPU 愿意为它停下来;哪一步漏了都不报错,只会安静地不响应。中断服务函数里 EXTI->PR = (1U<<0) 必须写:PR 是写 1 才清零、写 0 无效,不清的话退出中断后硬件认为还没处理完,会立刻再进一次,程序卡在死循环里。
好,按代码顺序逐行讲,手册位置在每块标题里。
第 1 行:头文件
引入 F103 头文件,RCC、GPIOA、GPIOC、AFIO、EXTI、SCB、NVIC 这些寄存器结构体都在里面定义,不包含编译器不认识。
块 1:开时钟(手册 6.3.7,P70)
逐符号:
RCC->APB2ENR:RCC 的 APB2 外设时钟使能寄存器,某位写 1 就给对应外设通电;1U<<0:bit0 = AFIOEN(AFIO 时钟);1U<<2:bit2 = IOPAEN(GPIOA 时钟);1U<<4:bit4 = IOPCEN(GPIOC 时钟);- 三个用
|合并,再用|=置 1,同时开三个时钟,其他位保持不变。
块 2:PA0 上拉输入(CRL 见 8.2.1 P113,ODR 见 8.2.4 P115)
CRL管 pin0~7,每个引脚占 4 位;0xF<<0对准 pin0 那 4 位,~取反后这 4 位变 0;&=把 pin0 的旧配置清成 0,其他位和 1 相与、不变。先清后写,避免旧值干扰。
0x8= 0b1000,写进 pin0 的 4 位;- 高 2 位 CNF=10 = 上拉/下拉输入,低 2 位 MODE=00 = 输入模式。
- ODR 平时是输出寄存器,但在"上拉/下拉"模式下被借来选方向:
- bit0 写 1 = 上拉(写 0 就是下拉)。
块 3:PC13 推挽输出(CRH 见 8.2.2 P114,BSRR 见 8.2.5 P115)
CRH管 pin8~15;pin13 的组内序号 = 13−8 = 5,偏移 = 5×4 = 20 位;- 清掉 bit20~23 这 4 位。
0x2= 0b0010:CNF=00 = 通用推挽,MODE=10 = 输出 2MHz。
- BSRR 低 16 位写 1 让对应 ODR 位置 1,而且是原子操作(不会被中断打断);
- PC13 输出高,板载 LED 低电平点亮,所以初始高 = 灭灯。
块 4:AFIO 把 Line0 接到 GPIOA(手册 8.4.3,P126)
EXTICR[0]就是 EXTICR1,管 Line0~3;写成数组[0]是因为头文件把 4 个 EXTICR 定义成数组;- 低 4 位决定 Line0 来自哪个端口:0=GPIOA、1=GPIOB、2=GPIOC……
- 先清再写 0,指定 Line0 用 PA0。
块 5:EXTI 下降沿 + 解除屏蔽(FTSR 见 9.3.4 P139,IMR 见 9.3.1 P138)
- FTSR 是下降沿触发选择寄存器,bit0=1 → Line0 下降沿触发(按键按下高变低)。
- IMR 是中断屏蔽寄存器,bit0=1 = 不屏蔽,中断信号才能送到 NVIC;写 0 就被挡住。
块 6:NVIC 内核寄存器(向量表见 9.1.2 P130)
- 高 16 位是键值 VECTKEY,必须写 0x5FA,否则整个寄存器被硬件拒绝;
- bit10:8 写 5 = Group2(2 位抢占 + 2 位子优先级)。
IP是优先级寄存器数组,每个中断占 1 字节;[6]因为 EXTI0 的中断号是 6;0x50= 0b01010000,F103 只用高 4 位:bit7:6=01 抢占优先级 1,bit5:4=01 子优先级 1。
- ISER 是中断使能寄存器,
[0]管 IRQ0~31; - bit6 写 1 → 使能 6 号中断,这是 NVIC 侧的总开关。
注意:
SCB、NVIC是 Cortex-M3 内核寄存器,不是 STM32 片上外设,RM0008 不列位定义,只在 9.1.2(P130)给向量表,逐位含义要查《Cortex-M3 技术参考手册》。
中断服务函数(向量表 9.1.2,P130)
函数名固定,启动文件向量表就指向这个名字,写错不被调用。
- PR 是挂起寄存器(9.3.6,P140),某位为 1 表示对应线产生了中断;
- 用
&测试 bit0,确认是 Line0 触发(共用函数里靠它区分是哪条线)。
^=异或翻转 bit13:1 变 0、0 变 1,LED 亮灭切换;- 中断里没有其他代码竞争这一位,读改写安全。
- PR 的特性是写 1 清零、写 0 无效;
- 这行清除挂起标志,不清的话退出后硬件认为没处理完,会立刻再进中断、死循环。
main 函数
整条链路串起来
开时钟(P70) → PA0上拉输入(P113/115)、PC13推挽输出(P114/115)
→ AFIO映射(P126) → 下降沿(P139)+解除屏蔽(P138)
→ 内核NVIC分组/优先级/使能
→ 按键:PR置1(P140) → 进中断翻转LED → 写1清PR → 返回空转
以下是你的寄存器代码逐段对应的标准库版本,每一段都标明了对应关系:
对应关系速查表
| 你的寄存器操作 | 标准库函数 |
|---|---|
RCC->APB2ENR |= ... | RCC_APB2PeriphClockCmd(...) |
GPIOA->CRL + GPIOA->ODR | GPIO_Init() + GPIO_Mode_IPU |
GPIOC->CRH + GPIOC->BSRR | GPIO_Init() + GPIO_SetBits() |
AFIO->EXTICR[0] | GPIO_EXTILineConfig() |
EXTI->FTSR + EXTI->IMR | EXTI_Init() |
SCB->AIRCR | NVIC_PriorityGroupConfig() |
NVIC->IP[6] + NVIC->ISER[0] | NVIC_Init() |
EXTI->PR 查询 | EXTI_GetITStatus() |
GPIOC->ODR ^= | GPIO_ToggleBits() |
EXTI->PR = 清除 | EXTI_ClearITPendingBit() |
核心差异总结
-
结构体传参:标准库把一堆寄存器位打包成结构体(如
GPIO_InitTypeDef),一次调用完成多个寄存器配置 -
宏封装:
GPIO_Pin_0、GPIO_Mode_IPU等宏替代了手算的位域值 -
函数名即注释:
GPIO_EXTILineConfig比AFIO->EXTICR[0] &= ~(0xFU<<0)更容易读懂意图 -
底层一样:标准库函数内部最终执行的仍是和你一模一样的寄存器操作
USART
这份代码做的是一个串口回环(echo)实验:电脑发什么,单片机就原样发回什么。
配置一共五步:开 GPIOA 与 USART1 的时钟 → PA9 设复用推挽当 TX、PA10 设上拉输入当 RX → 手算 BRR 定 115200 → CR1 先清零(8 位字长、无校验)再置 TE/RE → 最后置 UE 让外设通电。收发全是轮询:写 DR 之前等 TXE(bit7,发送数据寄存器空了才写,否则覆盖上一个字节),读 DR 之前等 RXNE(bit5,收到数据才读,读这个动作本身就把 RXNE 清掉)。主循环只有两句——收一个字节、原样发回一个字节,所以它既是验证串口链路通不通的最小实验,也是练 TXE / RXNE 手感的最短代码。
一、手册位置总表
| 手册位置 | 内容 |
|---|---|
| 25.3.4,P524 | 分数波特率公式与 BRR 换算 |
| 25.6.1,P540 | USART_SR(TXE/TC/RXNE/ORE) |
| 25.6.2,P541 | USART_DR |
| 25.6.3,P542 | USART_BRR |
| 25.6.4,P542 | USART_CR1(UE/TE/RE) |
| 6.3.7,P70 | RCC_APB2ENR |
二、BRR 自算过程(重点)
手册 P524 公式(USART1 挂 APB2,fCK = PCLK2 = 72MHz):
波特率 = fCK / (16 × USARTDIV)
USARTDIV = 72,000,000 / (16 × 115200) = 39.0625
- 整数部分 DIV_Mantissa = 39
- 小数部分 DIV_Fraction = 0.0625 × 16 = 1
- 合成:
BRR = (39 << 4) | 1 = 0x271
三、完整代码(注释内嵌手册位置)
四、四个状态位专题(P540 位表)
| 位 | 名称 | 含义 | 代码里的角色 |
|---|---|---|---|
| bit7 | TXE | 发送数据寄存器 TDR 空 | 写 DR 前等它,否则覆盖 |
| bit6 | TC | 发送完成(最后字节也移出了) | 见下方说明 |
| bit5 | RXNE | 接收数据寄存器非空 | 读 DR 前等它,读 DR 自动清零 |
| bit3 | ORE | 过载错误 | 见下方说明 |
TXE 和 TC 的区别(最容易混):
- TXE=1:数据已从 TDR 进了移位寄存器,TDR 空了,可以写下一个,但线上可能还在发;
- TC=1:连最后一个字节的移位都完成了,线上彻底发完。
- 连续发字符串时每个字节等 TXE 就行,只在最后一个字节等一次 TC(关外设或进低功耗前必须等 TC,否则最后字节发不全)。写法:
ORE 过载错误:
- 成因:上一个数据 RXNE 还没读走,新数据又来了,硬件置 ORE;
- 后果:移位寄存器的新数据被覆盖,但 RDR 里旧数据不丢;
- 清除:先读 SR,再读 DR(手册 P541)。你的轮询代码每收到立刻读走,正常不会触发;若担心,可在接收循环里检测 ORE 后读一次 DR 丢弃。
五、验证现象
-
串口助手设 115200、8N1,发送任意字符;
-
单片机收到后原样发回,助手收到同样字符,回环成立;
-
发中文/多字节也正常,因为每个字节都走同一套收发。
以下是你的 USART1 寄存器代码对应的标准库版本,逐段对应:
对应关系速查表
| 你的寄存器操作 | 标准库函数 |
|---|---|
RCC->APB2ENR |= (1U<<2) | (1U<<14) | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE) |
GPIOA->CRH 配 PA9 复用推挽 | GPIO_Init() + GPIO_Mode_AF_PP |
GPIOA->CRH + ODR 配 PA10 上拉输入 | GPIO_Init() + GPIO_Mode_IPU |
USART1->BRR = 0x271 | USART_InitStructure.USART_BaudRate = 115200 + USART_Init() |
USART1->CR1 清零 + 置位 TE/RE | USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx |
USART1->CR1 |= (1U<<13) 使能 | USART_Cmd(USART1, ENABLE) |
| 等待 TXE 标志 | while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET) |
| 写 DR 发送 | USART_SendData(USART1, data) |
| 等待 RXNE 标志 | while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET) |
| 读 DR 接收 | USART_ReceiveData(USART1) |
几个关键点
-
波特率 0x271:在 72MHz 时钟下对应 115200bps(72000000 / 16 / 115200 ≈ 39.0625 → 取整 39 = 0x27,小数部分 0.0625×16=1,所以 BRR = 0x0271)
-
标准库的
USART_Init一次性做了:-
配置 BRR(波特率)
-
配置 CR1(字长、奇偶校验、TE、RE)
-
配置 CR2(停止位)
-
配置 CR3(流控)
-
-
发送和接收函数:标准库的
USART_SendData和USART_ReceiveData就是对你手写的 DR 操作的封装,底层一模一样