C语言结构体与寄存器


单片机开发中免不了会与寄存器打交道。在51,AVR单片机中,会有一个头文件将寄存器的地址定义成更容易阅读的宏,在C语言中通过宏定义来访问寄存器。

#ifndef _AVR_IOM128_H_
#define _AVR_IOM128_H_ 1
............
/* Input Pins, Port D */
#define PIND      _SFR_IO8(0x10)

/* Data Direction Register, Port D */
#define DDRD      _SFR_IO8(0x11)

/* Data Register, Port D */
#define PORTD     _SFR_IO8(0x12)

/* Input Pins, Port C */
#define PINC      _SFR_IO8(0x13)

/* Data Direction Register, Port C */
#define DDRC      _SFR_IO8(0x14)

/* Data Register, Port C */
#define PORTC     _SFR_IO8(0x15)
...........

// When use GPIO register 
PORTD = 0xff; 
tmp = PINC;

而在STM32单片机中,寄存器不再是地址转化成宏定义的形式了,变成了一个结构体。对C语言不熟的同学可能会感到困惑。其实这样的定义方式,对于STM32单片机的寄存器设计而言,是更合理的一种方式。

#ifndef __STM32F10x_H
#define __STM32F10x_H
..........
/** 
  * @brief General Purpose I/O
  */

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
................

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
................


// When use GPIO register
GPIOA->ODR = 0xff;
tmp = GPIOB->IDR;

在C语言中,结构体对应着一种内存布局方式。定义一个结构体变量,表示申请了一块内存空间,这块内存将按照结构体定义的方式来布局。而定义一个结构体指针,表示这个指针指向的区域,会按照结构体定义的方式来布局。

typedef struct
{
    uint32_t field_0;
    uint16_t field_1;
    uint16_t field_2;
    uint32_t field_3;
}test_type_t;

test_type_t  var;
test_type_t  *p1;
test_type_t  *p2;

上面的结构体各字段在内存中布局如下:

如果反过来,我们知道一块内存布局,那也可以用一个结构体将其描述出来。在STM32单片机中,外设模块的功能寄存器就是一个特定的内存区域,有着特定的布局方式。根据器件的参考手册,就可以写出功能寄存器对应的结构体。

在STM32单片机中,同类外设的功能寄存器布局是相同的,通过不同的基地址来区分不同的外设。将不同的外设地址,赋值给不同的结构体指针,这些指针就代表了相应的外设。下面的图片是截取自STM32F103的参考手册,是关于GPIO寄存器基地址和寄存器的描述。

根据上面的图片,我们知道了STM32中GPIO寄存器的内存布局是下面这个样子,根据这个布局,我们可以反过来写出他的结构体定义。

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

再根据上面的基地址表格,不同的IO端口有着不同的基地址。将基地址转换成定义好的结构体类型,就得到了相应IO端口的寄存器定义。通过GPIOA->IDR的形式,就能访问各端口的寄存器了。

.......
#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
.......

这个时候再回过头来看STM32单片机的寄存器定义。先定义一个结构体,用来描述对应的外设功能寄存器布局,再将外设的基地址赋值给结构体指针。在后续的代码中,如果要使用这个外设的寄存器,通过这个结构体指针就能访问了。

GPIOA->ODR = 0xff;

上面的这段代码,理解起来就是有一个名为GPIOA的变量,他的地址是GPIOA_BASE,类型是GPIO_TypeDef的指针。把GPIOA的ODR设置为0xff,相当于以GPIOA_BASE为基础,加上ODR这个字段的偏移值,把这个地址的值设置为0xff。