C语言结构体与寄存器


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

  1. #ifndef _AVR_IOM128_H_
  2. #define _AVR_IOM128_H_ 1
  3. ............
  4. /* Input Pins, Port D */
  5. #define PIND _SFR_IO8(0x10)
  6. /* Data Direction Register, Port D */
  7. #define DDRD _SFR_IO8(0x11)
  8. /* Data Register, Port D */
  9. #define PORTD _SFR_IO8(0x12)
  10. /* Input Pins, Port C */
  11. #define PINC _SFR_IO8(0x13)
  12. /* Data Direction Register, Port C */
  13. #define DDRC _SFR_IO8(0x14)
  14. /* Data Register, Port C */
  15. #define PORTC _SFR_IO8(0x15)
  16. ...........
  17. // When use GPIO register
  18. PORTD = 0xff;
  19. tmp = PINC;
#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单片机的寄存器设计而言,是更合理的一种方式。

  1. #ifndef __STM32F10x_H
  2. #define __STM32F10x_H
  3. ..........
  4. /**
  5. * @brief General Purpose I/O
  6. */
  7. typedef struct
  8. {
  9. __IO uint32_t CRL;
  10. __IO uint32_t CRH;
  11. __IO uint32_t IDR;
  12. __IO uint32_t ODR;
  13. __IO uint32_t BSRR;
  14. __IO uint32_t BRR;
  15. __IO uint32_t LCKR;
  16. } GPIO_TypeDef;
  17. ................
  18. #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
  19. #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
  20. #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
  21. ................
  22. // When use GPIO register
  23. GPIOA->ODR = 0xff;
  24. tmp = GPIOB->IDR;
#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语言中,结构体对应着一种内存布局方式。定义一个结构体变量,表示申请了一块内存空间,这块内存将按照结构体定义的方式来布局。而定义一个结构体指针,表示这个指针指向的区域,会按照结构体定义的方式来布局。

  1. typedef struct
  2. {
  3. uint32_t field_0;
  4. uint16_t field_1;
  5. uint16_t field_2;
  6. uint32_t field_3;
  7. }test_type_t;
  8. test_type_t var;
  9. test_type_t *p1;
  10. test_type_t *p2;
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寄存器的内存布局是下面这个样子,根据这个布局,我们可以反过来写出他的结构体定义。

  1. typedef struct
  2. {
  3. __IO uint32_t CRL;
  4. __IO uint32_t CRH;
  5. __IO uint32_t IDR;
  6. __IO uint32_t ODR;
  7. __IO uint32_t BSRR;
  8. __IO uint32_t BRR;
  9. __IO uint32_t LCKR;
  10. } GPIO_TypeDef;
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的形式,就能访问各端口的寄存器了。

  1. .......
  2. #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
  3. #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
  4. #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
  5. .......
.......
#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
.......

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

  1. GPIOA->ODR = 0xff;
GPIOA->ODR = 0xff;

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