C语言结构体指针与硬件访问笔记(实战派)
2026/6/13 20:35:15 网站建设 项目流程

一、结构体指针的基础(复习用)

  1. 定义结构体指针
    先定义一个结构体:
cstructPoint{intx;inty;};

然后定义指针:

cstructPointp1={10,20};structPoint*ptr=&p1;// ptr指向p1
  1. 通过指针访问成员
    两种方式:

(*ptr).x (麻烦,要加括号)

ptr->x (箭头操作符,常用)

c

printf("%d",ptr->x);// 输出10ptr->y=30;// 修改y

注意:箭头 -> 左边必须是指针,点 . 左边是结构体变量本身。

  1. 结构体指针的指针运算和数组
    结构体指针也可以加加减减,移动一个结构体的大小。
cstructPointarr[3];structPoint*p=arr;// 指向第一个元素p++;// 现在指向arr[1]p->x=100;// 等价于 arr[1].x = 100

所以结构体指针遍历数组和普通指针一样。

二、为什么结构体指针重要?
效率:传指针比传整个结构体代价小(不用拷贝所有成员)。

修改原值:函数里想改外面的结构体,必须传指针。

硬件访问:硬件寄存器通常是一组连续地址,用结构体指针可以优雅地映射。

三、用结构体指针访问硬件(重点)

  1. 硬件寄存器是什么
    在单片机或外设里,控制寄存器、状态寄存器、数据寄存器等,都是分配在特定内存地址上的(内存映射IO)。
    比如:

控制寄存器在0x40021000

状态寄存器在0x40021004

数据寄存器在0x40021008

这些通常32位(4字节),地址连续。

  1. 用结构体映射一组寄存器
    我们可以定义一个结构体,成员顺序和偏移量与硬件手册一致。
c// 假设某个外设的寄存器映射typedefstruct{volatileunsignedintCR;// 控制寄存器,偏移0volatileunsignedintSR;// 状态寄存器,偏移4volatileunsignedintDR;// 数据寄存器,偏移8}USART_TypeDef;

然后把这个结构体“放在”硬件指定的基地址上:

c#defineUSART1_BASE0x40013800#defineUSART1((USART_TypeDef*)USART1_BASE)

现在 USART1 就是一个指向该外设的 结构体指针。

  1. 通过结构体指针操作硬件
c// 写控制寄存器USART1->CR=0x80;// 读状态寄存器if(USART1->SR&0x20){// 发送/接收数据}USART1->DR=data;

是不是很直观?不需要记偏移地址,直接用成员名。

注意:每个成员必须加volatile,因为硬件随时可能改变这些值,或者写操作有副作用,防止编译器优化。

四、结构体指针访问硬件的更多细节

  1. 为什么可以这样映射?
    C语言中,指针可以指向任意地址。我们把一个整型地址强制转换成 USART_TypeDef * 类型的指针,然后解引用(->)时,编译器会根据结构体成员的偏移量生成正确的地址。

等价的手动写法(不用结构体):

cvolatileunsignedint*cr=(unsignedint*)0x40013800;volatileunsignedint*sr=(unsignedint*)0x40013804;volatileunsignedint*dr=(unsignedint*)0x40013808;*cr=0x80;if(*sr&0x20)...

显然结构体指针更清晰。

  1. 结构体成员对齐问题
    硬件寄存器地址是严格的(比如连续4字节),编译器默认会对结构体成员对齐(比如按4字节对齐),这通常是好事,因为硬件也是4字节对齐的。
    但万一硬件不是自然对齐(比如2字节对齐),可以用attribute((packed)) 告诉编译器不要加填充。
ctypedefstruct__attribute__((packed)){volatileunsignedcharCR;volatileunsignedcharSR;volatileunsignedshortDR;}SimpleReg;

但访问非对齐的可能有性能问题,除非硬件手册明确说可以。一般寄存器都是自然对齐,不用担心。

  1. 位域(bit field)和结构体指针
    有些寄存器里的各个bit代表不同功能,可以用位域来定义。
ctypedefstruct{volatileunsignedintMODE:2;// bit0-1volatileunsignedintOUTPUT:1;// bit2volatileunsignedint:5;// 保留位,不命名}GPIO_CRL_Type;

然后用指针操作位域:

c#defineGPIOA_CRL((GPIO_CRL_Type*)0x40010800)GPIOA_CRL->MODE=2;// 设置低2位为2if(GPIOA_CRL->OUTPUT)...

注意:位域的可移植性不好(不同编译器对位域排列顺序可能不同),但如果只在特定单片机(比如STM32)上跑,可以放心用。很多官方库就是这么干的。

五、一个完整例子:点亮LED(假想的GPIO)
硬件假设:

GPIOA基地址0x40020000

偏移0:MODER(模式寄存器,32位)

偏移4:ODR(输出数据寄存器,32位)

PA0对应的bit是第0位

c// 1. 定义结构体typedefstruct{volatileunsignedintMODER;// 偏移0volatileunsignedintODR;// 偏移4// 其他省略}GPIO_TypeDef;// 2. 定义基址指针#defineGPIOA((GPIO_TypeDef*)0x40020000)// 3. 初始化:PA0设为输出模式(假设模式值0b01)GPIOA->MODER&=~(0x3<<0);// 先清零GPIOA->MODER|=(0x1<<0);// 设为输出// 4. 点亮LED(高电平有效)GPIOA->ODR|=(1<<0);// 5. 熄灭GPIOA->ODR&=~(1<<0);// 6. 翻转GPIOA->ODR^=(1<<0);

这段代码实际上和官方库的HAL底层很像,只是简化了。

六、结构体指针的常见坑(硬件领域)
地址是否有效
在应用层直接操作物理地址会崩溃(因为有MMU和操作系统)。只有在裸机或驱动开发里才这么干。在Linux里要用 ioremap 映射物理地址到虚拟地址,不能直接 (int *)0x…。

volatile不能少
不加volatile,编译优化可能:

你连续读两次SR,它可能只读一次,认为值不变。

你写CR的某位,可能被优化掉。

循环等待硬件标志位时变成死循环。

指针类型大小
结构体指针加1,到底跳多少?跳 sizeof(结构体)。确保硬件寄存器组之间没有间隙(或者间隙已通过保留成员处理)。如果硬件实际地址不连续,就不能用一个结构体指针遍历,要分别定义。

大小端问题
如果硬件寄存器是多字节,而你用结构体里成员是 unsigned char 数组逐字节访问,要注意大小端。一般用32位整型成员比较安全。

位域的顺序
比如 struct { unsigned int a:1; unsigned int b:1; },a是最低位还是高位?不同编译器可能相反。查手册或直接位操作。

不要对硬件结构体指针随意做加减
比如 USART1++ 会跳到下一个外设(如果地址连续),但一般不建议这样用,除非你明确知道地址是连续的且结构体大小匹配。

七、结构体指针和普通指针混用
有时候想按字节操作硬件,可以用 unsigned char * 指向同一个基地址。

c

GPIO_TypeDef*gpio=GPIOA;unsignedchar*byte_ptr=(unsignedchar*)gpio;// 比如改GPIOA基地址的第3个字节byte_ptr[2]=0xAA;

但要小心,这样容易破坏结构体成员对齐,只在调试或特殊需求时用。

八、总结(方便记忆)
结构体指针:ptr->member访问成员,比(*ptr).member方便。

硬件映射:定义结构体匹配寄存器布局,基址强转成结构体指针。

必须加 volatile:告诉编译器别乱优化。

结构体成员顺序:必须和硬件手册的偏移顺序一致(可加保留成员填补空隙)。

位域:方便但可移植性差,只在特定MCU用。

注意对齐和大小端。

裸机/驱动里直接用物理地址,有OS要用映射函数。

一个常用的套路:

c

typedefstruct{volatileuint32_tCR;volatileuint32_tCFGR;volatileuint32_tCIR;// ...}RCC_TypeDef;#defineRCC((RCC_TypeDef*)0x40021000)RCC->CR|=(1<<16);// 开启HSEwhile(!(RCC->CR&(1<<17)));// 等待就绪

差不多就这样。多看看厂商提供的库(比如STM32的HAL),里面全是这种写法,抄就完了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询