在 STM32 开发中,使用 C 语言定义外设寄存器是底层编程的基础。通常通过结构体将外设的多个寄存器组织在一起,再通过指针指向其基地址,从而实现对寄存器的读写操作。下面将穷举(详细列举)常见的定义方式,包括寄存器地址映射、结构体封装、位操作宏以及实际使用示例。
1. 定义外设基地址
每个外设都有一段连续的地址空间,首先需要定义其起始地址。
c
// 外设总线基地址(以 STM32F1 系列为例)
#define PERIPH_BASE ((uint32_t)0x40000000) // 外设总线基地址
#define APB1PERIPH_BASE PERIPH_BASE // APB1 总线
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) // APB2 总线
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000) // AHB 总线
// GPIO 外设基地址(位于 APB2 上)
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
// ... 其他 GPIO
// USART 外设基地址(位于 APB1 上)
#define USART1_BASE (APB2PERIPH_BASE + 0x3800) // USART1 在 APB2
#define USART2_BASE (APB1PERIPH_BASE + 0x4400)
#define USART3_BASE (APB1PERIPH_BASE + 0x4800)
2. 定义寄存器结构体
将外设的所有寄存器按照数据手册中的偏移顺序定义为结构体成员。
c
// GPIO 寄存器结构体
typedef struct {
volatile uint32_t CRL; // 端口配置低寄存器,偏移 0x00
volatile uint32_t CRH; // 端口配置高寄存器,偏移 0x04
volatile uint32_t IDR; // 输入数据寄存器,偏移 0x08
volatile uint32_t ODR; // 输出数据寄存器,偏移 0x0C
volatile uint32_t BSRR; // 位设置/清除寄存器,偏移 0x10
volatile uint32_t BRR; // 位清除寄存器,偏移 0x14
volatile uint32_t LCKR; // 配置锁定寄存器,偏移 0x18
} GPIO_TypeDef;
// USART 寄存器结构体
typedef struct {
volatile uint32_t SR; // 状态寄存器,偏移 0x00
volatile uint32_t DR; // 数据寄存器,偏移 0x04
volatile uint32_t BRR; // 波特率寄存器,偏移 0x08
volatile uint32_t CR1; // 控制寄存器1,偏移 0x0C
volatile uint32_t CR2; // 控制寄存器2,偏移 0x10
volatile uint32_t CR3; // 控制寄存器3,偏移 0x14
volatile uint32_t GTPR; // 保护时间和预分频寄存器,偏移 0x18
} USART_TypeDef;
3. 将基地址转换为结构体指针
通过强制类型转换,使指针指向外设的基地址,之后就可以通过指针访问寄存器。
c
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define USART1 ((USART_TypeDef *) USART1_BASE)
#define USART2 ((USART_TypeDef *) USART2_BASE)
4. 定义位操作宏
为了方便设置/清除寄存器的某一位,常定义位掩码或位号。
c
// GPIO 引脚位定义
#define GPIO_Pin_0 ((uint16_t)0x0001)
#define GPIO_Pin_1 ((uint16_t)0x0002)
// ...
#define GPIO_Pin_15 ((uint16_t)0x8000)
// 寄存器位定义(例如 USART SR 寄存器)
#define USART_SR_TXE ((uint16_t)0x0080) // 发送数据寄存器空
#define USART_SR_TC ((uint16_t)0x0040) // 发送完成
#define USART_SR_RXNE ((uint16_t)0x0020) // 读数据寄存器非空
5. 定义操作寄存器的宏或函数
常用操作如设置位、清除位、读取位等,可以用宏封装。
c
// 设置寄存器的某些位
#define SET_BIT(REG, BIT) ((REG) |= (BIT))
// 清除寄存器的某些位
#define CLEAR_BIT(REG, BIT) ((REG) &= ~(BIT))
// 读取寄存器的某些位
#define READ_BIT(REG, BIT) ((REG) & (BIT))
// 写入寄存器的值
#define WRITE_REG(REG, VAL) ((REG) = (VAL))
// 读取寄存器的值
#define READ_REG(REG) ((REG))
6. 实际使用示例
6.1 GPIO 初始化与操作
c
// 使能 GPIOA 时钟(假设 RCC 寄存器已定义)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 配置 PA0 为推挽输出,最大速度 50MHz
GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0); // 先清除原有配置
GPIOA->CRL |= GPIO_CRL_MODE0_1 | GPIO_CRL_MODE0_0; // 设置模式为 50MHz 输出
// CNF 保持 00 表示通用推挽输出
// 设置 PA0 输出高电平
GPIOA->BSRR = GPIO_Pin_0;
// 设置 PA0 输出低电平
GPIOA->BRR = GPIO_Pin_0;
// 读取 PA1 输入电平
uint8_t pin_state = (GPIOA->IDR & GPIO_Pin_1) ? 1 : 0;
6.2 USART 发送数据
c
// 使能 USART1 时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
// 配置波特率、数据位等(略)
// 发送一个字节
void USART_SendChar(USART_TypeDef* USARTx, uint8_t ch) {
// 等待发送数据寄存器空
while (!(USARTx->SR & USART_SR_TXE));
USARTx->DR = ch;
}
// 使用
USART_SendChar(USART1, 'A');
7. 更高级的定义方式
7.1 使用位域结构体(不推荐,因为位域的顺序和端序可能因编译器而异,但有时用于精确控制)
c
typedef struct {
volatile uint32_t MODER : 2; // 模式
volatile uint32_t OTYPER : 1; // 输出类型
volatile uint32_t OSPEEDR: 2; // 输出速度
volatile uint32_t PUPD : 2; // 上拉/下拉
volatile uint32_t IDR : 1; // 输入数据
volatile uint32_t ODR : 1; // 输出数据
// ... 其他位
} GPIO_Pin_TypeDef;
但这种方式不常用,因为 STM32 标准库和 HAL 库都采用宏和结构体方式。
7.2 定义中断向量表
c
typedef void (*pFunction)(void); // 函数指针类型
typedef struct {
uint32_t stack_top; // 栈顶地址
pFunction Reset_Handler; // 复位处理函数
pFunction NMI_Handler; // NMI 处理函数
pFunction HardFault_Handler; // 硬错误处理函数
// ... 其他异常和中断向量
} VectorTable;
// 在链接脚本中分配向量表到起始地址
extern VectorTable __Vectors;
8. 常见的 C 语言语句在 STM32 编程中的应用
除了上述定义外,实际编程中会大量使用以下 C 语言语句:
- 变量定义 :
uint32_t timeout; - 循环 :
while(!(USART1->SR & USART_SR_RXNE)); - 条件判断 :
if(GPIOA->IDR & GPIO_Pin_0) { ... } - 函数定义 :
void delay(volatile uint32_t count) { while(count--); } - 宏定义 :
#define LED_ON() GPIOA->BSRR = GPIO_Pin_5 - 指针操作 :
uint32_t *p = (uint32_t *)0x20000000; - 结构体访问 :
GPIOA->CRL = 0x44444444; - 位运算 :
TIM2->CR1 |= TIM_CR1_CEN;
总结
在 STM32 的 C 语言编程中,定义外设的核心步骤是:
- 用
#define定义外设的基地址。 - 用
typedef struct定义外设的寄存器映射。 - 用
#define将基地址转换为结构体指针,得到外设访问句柄。 - 用
#define定义各寄存器中的位掩码或功能常量。 - 使用上述定义进行寄存器读写和位操作。
这些定义通常放在芯片型号对应的头文件中(如 stm32f10x.h),开发者直接包含即可使用。通过这种方式,C 语言能够简洁且高效地操作 STM32 的硬件资源。