STM32 Register second layer · RM0008 · hand-written
第二层 · 寄存器

STM32 / register layer · F103C8T6

STM32 寄存器学习

只凭 RM0008 手写 与学习版、背诵版同一套批注与同步

纯手写GPIO寄存器

以下是完整的单文件 main.c,所有代码和寄存器操作都在一个文件中,可直接编译运行:

C64 lines
// ================= 宏定义区域 =================
#define GPIOB_BASE (unsigned int)0x40010C00
#define RCC_BASE (unsigned int)0x40021000
#define RCC_APB2ENR *((volatile unsigned int*)(RCC_BASE+0x18))
#define GPIOB_CRL *((volatile unsigned int*)(GPIOB_BASE+0x00))
#define GPIOB_ODR *((volatile unsigned int*)(GPIOB_BASE+0x0c))
#define GPIOB_BSRR *((volatile unsigned int*)(GPIOB_BASE+0x10))
#define LED_PIN 5
#define RCC_IOPBEN (1 << 3)
// ================= 函数声明 =================
void led_init(void);
void led_on(void);
void led_off(void);
void delay(volatile unsigned int count);
// ================= 函数实现 =================
void led_init(void)
{
// 1. 使能 GPIOB 时钟 (RCC_APB2ENR 位3)
RCC_APB2ENR |= RCC_IOPBEN;
// 2. 配置 PB5 为推挽输出 50MHz
// CRL 中 PB5 对应位 23:20,先清零再写入 0b0011
GPIOB_CRL &= ~(0xF << (4 * LED_PIN));
GPIOB_CRL |= (0x3 << (4 * LED_PIN));
// 3. 初始灭灯(高电平),使用 BSRR 原子置位 BS5
GPIOB_BSRR = (1 << LED_PIN);
}
void led_on(void)
{
// 低电平亮:BSRR 高16位 BR5 原子清除 (1 << 21)
GPIOB_BSRR = (1 << (LED_PIN + 16));
}
void led_off(void)
{
// 高电平灭:BSRR 低16位 BS5 原子置位 (1 << 5)
GPIOB_BSRR = (1 << LED_PIN);
}
void delay(volatile unsigned int count)
{
while(count--);
}
// ================= 主函数 =================
int main(void)
{
led_init();
while(1)
{
led_on();
delay(5000000);
led_off();
delay(5000000);
}
}

代码特点说明

  1. 纯寄存器操作:所有外设访问均通过宏定义的寄存器地址直接操作,未使用任何库函数。

  2. 原子操作:LED 亮灭使用 BSRR 寄存器,避免了 ODR 读-改-写的中断风险。

  3. 可移植性:修改 LED_PIN 和对应的 RCC_APB2ENR 位即可更换引脚。

  4. 延时函数:使用简单的循环延时,未使用定时器(后续学习 TIM 后可替换为精确延时)。

  5. 注释完整:每行关键操作都有详细注释,便于理解和复习。

编译注意事项

  • 需要包含 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 无效,不清的话退出中断后硬件认为还没处理完,会立刻再进一次,程序卡在死循环里。

C47 lines
#include "stm32f10x.h"
void My_EXTI_Reg_Init(void)
{
/*===== 1. 开时钟 —— RCC_APB2ENR(手册6.3.7,P70)=====
bit0=AFIOEN、bit2=IOPAEN、bit4=IOPCEN,置1通电,不碰其他位 */
RCC->APB2ENR |= (1U<<0) | (1U<<2) | (1U<<4);
/*===== 2. PA0 上拉输入 —— CRL(8.2.1,P113)+ ODR(8.2.4,P115)=====*/
GPIOA->CRL &= ~(0xFU<<0); /* 清pin0的4位旧配置 */
GPIOA->CRL |= (0x8U<<0); /* 0x8=1000:CNF=10上拉/下拉,MODE=00输入 */
GPIOA->ODR |= (1U<<0); /* ODR0=1,选上拉(0则下拉) */
/*===== 3. PC13 推挽输出 —— CRH(8.2.2,P114)+ BSRR(8.2.5,P115)=====*/
GPIOC->CRH &= ~(0xFU<<20); /* pin13在CRH组内序号5,偏移=5*4=20 */
GPIOC->CRH |= (0x2U<<20); /* 0x2=0010:CNF=00推挽,MODE=10=2MHz */
GPIOC->BSRR = (1U<<13); /* 原子置位,PC13初始高=灭(低电平点亮) */
/*===== 4. AFIO 把 Line0 接到 GPIOA —— EXTICR1(8.4.3,P126)=====*/
AFIO->EXTICR[0] &= ~(0xFU<<0);
AFIO->EXTICR[0] |= (0x0U<<0); /* 0=GPIOA(1=GPIOB,2=GPIOC…) */
/*===== 5. EXTI 下降沿 + 解除屏蔽 —— FTSR(9.3.4,P139)、IMR(9.3.1,P138)=====*/
EXTI->FTSR |= (1U<<0); /* Line0 下降沿:按键按下高->低 */
EXTI->IMR |= (1U<<0); /* 不屏蔽,中断才能送NVIC */
/*===== 6. NVIC(内核寄存器,见下方说明;向量表见9.1.2,P130)=====*/
SCB->AIRCR = (0x5FAU<<16) | (5U<<8); /* 键值0x5FA + PRIGROUP=5(Group2:2抢占+2子)*/
NVIC->IP[6] = 0x50; /* 中断号6,高4位0101=抢占1、子1 */
NVIC->ISER[0] = (1U<<6); /* 使能6号中断 */
}
/*===== 中断服务函数(函数名固定,向量表9.1.2,P130)=====*/
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & (1U<<0)) /* PR(9.3.6,P140):确认Line0挂起 */
{
GPIOC->ODR ^= (1U<<13); /* 翻转LED(ODR,8.2.4,P115)*/
EXTI->PR = (1U<<0); /* PR写1清挂起(写1才清),否则重复进中断 */
}
}
int main(void)
{
My_EXTI_Reg_Init();
while (1) { } /* 空转,按键全靠中断 */
}

好,按代码顺序逐行讲,手册位置在每块标题里。

第 1 行:头文件

C1 lines
#include "stm32f10x.h"

引入 F103 头文件,RCC、GPIOA、GPIOC、AFIO、EXTI、SCB、NVIC 这些寄存器结构体都在里面定义,不包含编译器不认识。

块 1:开时钟(手册 6.3.7,P70)

C1 lines
RCC->APB2ENR |= (1U<<0) | (1U<<2) | (1U<<4);

逐符号:

  • 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)

C1 lines
GPIOA->CRL &= ~(0xFU<<0);
  • CRL 管 pin0~7,每个引脚占 4 位;
  • 0xF<<0 对准 pin0 那 4 位,~ 取反后这 4 位变 0;
  • &= 把 pin0 的旧配置清成 0,其他位和 1 相与、不变。先清后写,避免旧值干扰。
C1 lines
GPIOA->CRL |= (0x8U<<0);
  • 0x8 = 0b1000,写进 pin0 的 4 位;
  • 高 2 位 CNF=10 = 上拉/下拉输入,低 2 位 MODE=00 = 输入模式。
C1 lines
GPIOA->ODR |= (1U<<0);
  • ODR 平时是输出寄存器,但在"上拉/下拉"模式下被借来选方向:
  • bit0 写 1 = 上拉(写 0 就是下拉)。

块 3:PC13 推挽输出(CRH 见 8.2.2 P114,BSRR 见 8.2.5 P115)

C1 lines
GPIOC->CRH &= ~(0xFU<<20);
  • CRH 管 pin8~15;pin13 的组内序号 = 13−8 = 5,偏移 = 5×4 = 20 位;
  • 清掉 bit20~23 这 4 位。
C1 lines
GPIOC->CRH |= (0x2U<<20);
  • 0x2 = 0b0010:CNF=00 = 通用推挽,MODE=10 = 输出 2MHz。
C1 lines
GPIOC->BSRR = (1U<<13);
  • BSRR 低 16 位写 1 让对应 ODR 位置 1,而且是原子操作(不会被中断打断);
  • PC13 输出高,板载 LED 低电平点亮,所以初始高 = 灭灯。

块 4:AFIO 把 Line0 接到 GPIOA(手册 8.4.3,P126)

C2 lines
AFIO->EXTICR[0] &= ~(0xFU<<0);
AFIO->EXTICR[0] |= (0x0U<<0);
  • 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)

C1 lines
EXTI->FTSR |= (1U<<0);
  • FTSR 是下降沿触发选择寄存器,bit0=1 → Line0 下降沿触发(按键按下高变低)。
C1 lines
EXTI->IMR |= (1U<<0);
  • IMR 是中断屏蔽寄存器,bit0=1 = 不屏蔽,中断信号才能送到 NVIC;写 0 就被挡住。

块 6:NVIC 内核寄存器(向量表见 9.1.2 P130)

C1 lines
SCB->AIRCR = (0x5FAU<<16) | (5U<<8);
  • 高 16 位是键值 VECTKEY,必须写 0x5FA,否则整个寄存器被硬件拒绝;
  • bit10:8 写 5 = Group2(2 位抢占 + 2 位子优先级)。
C1 lines
NVIC->IP[6] = 0x50;
  • IP 是优先级寄存器数组,每个中断占 1 字节;[6] 因为 EXTI0 的中断号是 6;
  • 0x50 = 0b01010000,F103 只用高 4 位:bit7:6=01 抢占优先级 1,bit5:4=01 子优先级 1。
C1 lines
NVIC->ISER[0] = (1U<<6);
  • ISER 是中断使能寄存器,[0] 管 IRQ0~31;
  • bit6 写 1 → 使能 6 号中断,这是 NVIC 侧的总开关。

注意:SCB、NVIC 是 Cortex-M3 内核寄存器,不是 STM32 片上外设,RM0008 不列位定义,只在 9.1.2(P130)给向量表,逐位含义要查《Cortex-M3 技术参考手册》。

中断服务函数(向量表 9.1.2,P130)

C1 lines
void EXTI0_IRQHandler(void)

函数名固定,启动文件向量表就指向这个名字,写错不被调用。

C1 lines
if (EXTI->PR & (1U<<0))
  • PR 是挂起寄存器(9.3.6,P140),某位为 1 表示对应线产生了中断;
  • 用 & 测试 bit0,确认是 Line0 触发(共用函数里靠它区分是哪条线)。
C1 lines
GPIOC->ODR ^= (1U<<13);
  • ^= 异或翻转 bit13:1 变 0、0 变 1,LED 亮灭切换;
  • 中断里没有其他代码竞争这一位,读改写安全。
C1 lines
EXTI->PR = (1U<<0);
  • PR 的特性是写 1 清零、写 0 无效;
  • 这行清除挂起标志,不清的话退出后硬件认为没处理完,会立刻再进中断、死循环。

main 函数

C5 lines
int main(void)
{
My_EXTI_Reg_Init(); // 执行上面全部配置
while (1) { } // 空转,按键动作全部由中断异步完成
}

整条链路串起来

开时钟(P70) → PA0上拉输入(P113/115)、PC13推挽输出(P114/115)
           → AFIO映射(P126) → 下降沿(P139)+解除屏蔽(P138)
           → 内核NVIC分组/优先级/使能
           → 按键:PR置1(P140) → 进中断翻转LED → 写1清PR → 返回空转

以下是你的寄存器代码逐段对应的标准库版本,每一段都标明了对应关系:

C68 lines
#include "stm32f10x.h"
void My_EXTI_StdPeriph_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/*===== 1. 开时钟 =====*/
/* 寄存器: RCC->APB2ENR |= (1U<<0) | (1U<<2) | (1U<<4); */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);
/*===== 2. PA0 上拉输入 =====*/
/* 寄存器: GPIOA->CRL &= ~(0xFU<<0); GPIOA->CRL |= (0x8U<<0); GPIOA->ODR |= (1U<<0); */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*===== 3. PC13 推挽输出 =====*/
/* 寄存器: GPIOC->CRH &= ~(0xFU<<20); GPIOC->CRH |= (0x2U<<20); GPIOC->BSRR = (1U<<13); */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 2MHz
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_SetBits(GPIOC, GPIO_Pin_13); // 初始高电平(灭)
/*===== 4. AFIO 把 Line0 接到 GPIOA =====*/
/* 寄存器: AFIO->EXTICR[0] &= ~(0xFU<<0); AFIO->EXTICR[0] |= (0x0U<<0); */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
/*===== 5. EXTI 下降沿 + 解除屏蔽 =====*/
/* 寄存器: EXTI->FTSR |= (1U<<0); EXTI->IMR |= (1U<<0); */
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/*===== 6. NVIC =====*/
/* 寄存器: SCB->AIRCR = (0x5FAU<<16) | (5U<<8); NVIC->IP[6] = 0x50; NVIC->ISER[0] = (1U<<6); */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占+2位子优先级
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; // 中断号6
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/*===== 中断服务函数 =====*/
void EXTI0_IRQHandler(void)
{
/* 寄存器: if (EXTI->PR & (1U<<0)) */
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
/* 寄存器: GPIOC->ODR ^= (1U<<13); */
GPIO_ToggleBits(GPIOC, GPIO_Pin_13);
/* 寄存器: EXTI->PR = (1U<<0); */
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
int main(void)
{
My_EXTI_StdPeriph_Init();
while (1) { }
}

对应关系速查表

你的寄存器操作标准库函数
RCC->APB2ENR |= ...RCC_APB2PeriphClockCmd(...)
GPIOA->CRL + GPIOA->ODRGPIO_Init() + GPIO_Mode_IPU
GPIOC->CRH + GPIOC->BSRRGPIO_Init() + GPIO_SetBits()
AFIO->EXTICR[0]GPIO_EXTILineConfig()
EXTI->FTSR + EXTI->IMREXTI_Init()
SCB->AIRCRNVIC_PriorityGroupConfig()
NVIC->IP[6] + NVIC->ISER[0]NVIC_Init()
EXTI->PR 查询EXTI_GetITStatus()
GPIOC->ODR ^=GPIO_ToggleBits()
EXTI->PR = 清除EXTI_ClearITPendingBit()

核心差异总结

  1. 结构体传参:标准库把一堆寄存器位打包成结构体(如 GPIO_InitTypeDef),一次调用完成多个寄存器配置

  2. 宏封装:GPIO_Pin_0、GPIO_Mode_IPU 等宏替代了手算的位域值

  3. 函数名即注释:GPIO_EXTILineConfig 比 AFIO->EXTICR[0] &= ~(0xFU<<0) 更容易读懂意图

  4. 底层一样:标准库函数内部最终执行的仍是和你一模一样的寄存器操作

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

三、完整代码(注释内嵌手册位置)

C49 lines
#include "stm32f10x.h"
void My_USART1_Reg_Init(void)
{
/* 1. 开时钟 RCC_APB2ENR(6.3.7,P70):IOPAEN=bit2、USART1EN=bit14 */
RCC->APB2ENR |= (1U<<2) | (1U<<14);
/* 2. PA9=TX 复用推挽2MHz(CRH,第8章):pin9组内序号1,偏移4,值0xB */
GPIOA->CRH &= ~(0xFU<<4);
GPIOA->CRH |= (0xBU<<4);
/* 3. PA10=RX 上拉输入:pin10组内序号2,偏移8,值0x8 + ODR置1 */
GPIOA->CRH &= ~(0xFU<<8);
GPIOA->CRH |= (0x8U<<8);
GPIOA->ODR |= (1U<<10);
/* 4. 波特率 USART_BRR(25.6.3,P542)*/
USART1->BRR = 0x271;
/* 5. CR1(25.6.4,P542):先清=8位无校验,再 TE=bit3、RE=bit2,最后 UE=bit13 */
USART1->CR1 = 0;
USART1->CR1 |= (1U<<3) | (1U<<2);
USART1->CR1 |= (1U<<13);
}
/* 发送一个字节:等TXE → 写DR */
void My_USART1_SendByte(uint8_t data)
{
while ((USART1->SR & (1U<<7)) == 0); /* TXE=1 才能写 */
USART1->DR = data;
}
/* 接收一个字节:等RXNE → 读DR(读DR自动清RXNE)*/
uint8_t My_USART1_ReceiveByte(void)
{
while ((USART1->SR & (1U<<5)) == 0); /* RXNE=1 才有数据 */
return (uint8_t)USART1->DR;
}
int main(void)
{
uint8_t ch;
My_USART1_Reg_Init();
while (1)
{
ch = My_USART1_ReceiveByte(); /* 收一个 */
My_USART1_SendByte(ch); /* 发回(回环)*/
}
}

四、四个状态位专题(P540 位表)

位 名称 含义 代码里的角色
bit7 TXE 发送数据寄存器 TDR 空 写 DR 前等它,否则覆盖
bit6 TC 发送完成(最后字节也移出了) 见下方说明
bit5 RXNE 接收数据寄存器非空 读 DR 前等它,读 DR 自动清零
bit3 ORE 过载错误 见下方说明

TXE 和 TC 的区别(最容易混):

  • TXE=1:数据已从 TDR 进了移位寄存器,TDR 空了,可以写下一个,但线上可能还在发;
  • TC=1:连最后一个字节的移位都完成了,线上彻底发完。
  • 连续发字符串时每个字节等 TXE 就行,只在最后一个字节等一次 TC(关外设或进低功耗前必须等 TC,否则最后字节发不全)。写法:
C1 lines
while ((USART1->SR & (1U<<6)) == 0); /* 等TC,放在发送结束处 */

ORE 过载错误:

  • 成因:上一个数据 RXNE 还没读走,新数据又来了,硬件置 ORE;
  • 后果:移位寄存器的新数据被覆盖,但 RDR 里旧数据不丢;
  • 清除:先读 SR,再读 DR(手册 P541)。你的轮询代码每收到立刻读走,正常不会触发;若担心,可在接收循环里检测 ORE 后读一次 DR 丢弃。

五、验证现象

  1. 串口助手设 115200、8N1,发送任意字符;

  2. 单片机收到后原样发回,助手收到同样字符,回环成立;

  3. 发中文/多字节也正常,因为每个字节都走同一套收发。

以下是你的 USART1 寄存器代码对应的标准库版本,逐段对应:

C71 lines
#include "stm32f10x.h"
#include "stm32f10x_usart.h" // 标准库 USART 头文件
void My_USART1_StdPeriph_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/*===== 1. 开时钟 =====*/
/* 寄存器: RCC->APB2ENR |= (1U<<2) | (1U<<14); */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
/*===== 2. PA9 = TX 复用推挽输出 2MHz =====*/
/* 寄存器: GPIOA->CRH &= ~(0xFU<<4); GPIOA->CRH |= (0xBU<<4); */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*===== 3. PA10 = RX 上拉输入 =====*/
/* 寄存器: GPIOA->CRH &= ~(0xFU<<8); GPIOA->CRH |= (0x8U<<8); GPIOA->ODR |= (1U<<10); */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 输入模式下 Speed 无效,习惯保留
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*===== 4. 配置 USART1 参数 =====*/
/* 寄存器: USART1->BRR = 0x271; (72MHz 下 115200 波特率)*/
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8 位数据
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1 位停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无流控
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 收发都开
USART_Init(USART1, &USART_InitStructure);
/*===== 5. 使能 USART1 =====*/
/* 寄存器: USART1->CR1 |= (1U<<13); */
USART_Cmd(USART1, ENABLE);
}
/* 发送一个字节 */
void My_USART1_SendByte(uint8_t data)
{
/* 寄存器: while ((USART1->SR & (1U<<7)) == 0); */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
/* 寄存器: USART1->DR = data; */
USART_SendData(USART1, data);
}
/* 接收一个字节 */
uint8_t My_USART1_ReceiveByte(void)
{
/* 寄存器: while ((USART1->SR & (1U<<5)) == 0); */
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
/* 寄存器: return (uint8_t)USART1->DR; */
return USART_ReceiveData(USART1);
}
int main(void)
{
uint8_t ch;
My_USART1_StdPeriph_Init();
while (1)
{
ch = My_USART1_ReceiveByte();
My_USART1_SendByte(ch);
}
}

对应关系速查表

你的寄存器操作标准库函数
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 = 0x271USART_InitStructure.USART_BaudRate = 115200 + USART_Init()
USART1->CR1 清零 + 置位 TE/REUSART_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)

几个关键点

  1. 波特率 0x271:在 72MHz 时钟下对应 115200bps(72000000 / 16 / 115200 ≈ 39.0625 → 取整 39 = 0x27,小数部分 0.0625×16=1,所以 BRR = 0x0271)

  2. 标准库的 USART_Init 一次性做了:

    • 配置 BRR(波特率)

    • 配置 CR1(字长、奇偶校验、TE、RE)

    • 配置 CR2(停止位)

    • 配置 CR3(流控)

  3. 发送和接收函数:标准库的 USART_SendData 和 USART_ReceiveData 就是对你手写的 DR 操作的封装,底层一模一样