C语言结构体指针与硬件访问笔记(实战派)

C语言结构体指针与硬件访问笔记(实战派) 一、结构体指针的基础复习用定义结构体指针先定义一个结构体cstructPoint{intx;inty;};然后定义指针cstructPointp1{10,20};structPoint*ptrp1;// ptr指向p1通过指针访问成员两种方式(*ptr).x 麻烦要加括号ptr-x 箭头操作符常用cprintf(%d,ptr-x);// 输出10ptr-y30;// 修改y注意箭头 - 左边必须是指针点 . 左边是结构体变量本身。结构体指针的指针运算和数组结构体指针也可以加加减减移动一个结构体的大小。cstructPointarr[3];structPoint*parr;// 指向第一个元素p;// 现在指向arr[1]p-x100;// 等价于 arr[1].x 100所以结构体指针遍历数组和普通指针一样。二、为什么结构体指针重要效率传指针比传整个结构体代价小不用拷贝所有成员。修改原值函数里想改外面的结构体必须传指针。硬件访问硬件寄存器通常是一组连续地址用结构体指针可以优雅地映射。三、用结构体指针访问硬件重点硬件寄存器是什么在单片机或外设里控制寄存器、状态寄存器、数据寄存器等都是分配在特定内存地址上的内存映射IO。比如控制寄存器在0x40021000状态寄存器在0x40021004数据寄存器在0x40021008这些通常32位4字节地址连续。用结构体映射一组寄存器我们可以定义一个结构体成员顺序和偏移量与硬件手册一致。c// 假设某个外设的寄存器映射typedefstruct{volatileunsignedintCR;// 控制寄存器偏移0volatileunsignedintSR;// 状态寄存器偏移4volatileunsignedintDR;// 数据寄存器偏移8}USART_TypeDef;然后把这个结构体“放在”硬件指定的基地址上c#defineUSART1_BASE0x40013800#defineUSART1((USART_TypeDef*)USART1_BASE)现在 USART1 就是一个指向该外设的 结构体指针。通过结构体指针操作硬件c// 写控制寄存器USART1-CR0x80;// 读状态寄存器if(USART1-SR0x20){// 发送/接收数据}USART1-DRdata;是不是很直观不需要记偏移地址直接用成员名。注意每个成员必须加volatile因为硬件随时可能改变这些值或者写操作有副作用防止编译器优化。四、结构体指针访问硬件的更多细节为什么可以这样映射C语言中指针可以指向任意地址。我们把一个整型地址强制转换成 USART_TypeDef * 类型的指针然后解引用-时编译器会根据结构体成员的偏移量生成正确的地址。等价的手动写法不用结构体cvolatileunsignedint*cr(unsignedint*)0x40013800;volatileunsignedint*sr(unsignedint*)0x40013804;volatileunsignedint*dr(unsignedint*)0x40013808;*cr0x80;if(*sr0x20)...显然结构体指针更清晰。结构体成员对齐问题硬件寄存器地址是严格的比如连续4字节编译器默认会对结构体成员对齐比如按4字节对齐这通常是好事因为硬件也是4字节对齐的。但万一硬件不是自然对齐比如2字节对齐可以用attribute((packed)) 告诉编译器不要加填充。ctypedefstruct__attribute__((packed)){volatileunsignedcharCR;volatileunsignedcharSR;volatileunsignedshortDR;}SimpleReg;但访问非对齐的可能有性能问题除非硬件手册明确说可以。一般寄存器都是自然对齐不用担心。位域bit field和结构体指针有些寄存器里的各个bit代表不同功能可以用位域来定义。ctypedefstruct{volatileunsignedintMODE:2;// bit0-1volatileunsignedintOUTPUT:1;// bit2volatileunsignedint:5;// 保留位不命名}GPIO_CRL_Type;然后用指针操作位域c#defineGPIOA_CRL((GPIO_CRL_Type*)0x40010800)GPIOA_CRL-MODE2;// 设置低2位为2if(GPIOA_CRL-OUTPUT)...注意位域的可移植性不好不同编译器对位域排列顺序可能不同但如果只在特定单片机比如STM32上跑可以放心用。很多官方库就是这么干的。五、一个完整例子点亮LED假想的GPIO硬件假设GPIOA基地址0x40020000偏移0MODER模式寄存器32位偏移4ODR输出数据寄存器32位PA0对应的bit是第0位c// 1. 定义结构体typedefstruct{volatileunsignedintMODER;// 偏移0volatileunsignedintODR;// 偏移4// 其他省略}GPIO_TypeDef;// 2. 定义基址指针#defineGPIOA((GPIO_TypeDef*)0x40020000)// 3. 初始化PA0设为输出模式假设模式值0b01GPIOA-MODER~(0x30);// 先清零GPIOA-MODER|(0x10);// 设为输出// 4. 点亮LED高电平有效GPIOA-ODR|(10);// 5. 熄灭GPIOA-ODR~(10);// 6. 翻转GPIOA-ODR^(10);这段代码实际上和官方库的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 * 指向同一个基地址。cGPIO_TypeDef*gpioGPIOA;unsignedchar*byte_ptr(unsignedchar*)gpio;// 比如改GPIOA基地址的第3个字节byte_ptr[2]0xAA;但要小心这样容易破坏结构体成员对齐只在调试或特殊需求时用。八、总结方便记忆结构体指针ptr-member访问成员比(*ptr).member方便。硬件映射定义结构体匹配寄存器布局基址强转成结构体指针。必须加 volatile告诉编译器别乱优化。结构体成员顺序必须和硬件手册的偏移顺序一致可加保留成员填补空隙。位域方便但可移植性差只在特定MCU用。注意对齐和大小端。裸机/驱动里直接用物理地址有OS要用映射函数。一个常用的套路ctypedefstruct{volatileuint32_tCR;volatileuint32_tCFGR;volatileuint32_tCIR;// ...}RCC_TypeDef;#defineRCC((RCC_TypeDef*)0x40021000)RCC-CR|(116);// 开启HSEwhile(!(RCC-CR(117)));// 等待就绪差不多就这样。多看看厂商提供的库比如STM32的HAL里面全是这种写法抄就完了。