C语言嵌入式OOP实践:I²C驱动与EEPROM设备封装
1. 面向对象思想在嵌入式I²C驱动开发中的工程实践在资源受限的嵌入式系统中C语言长期占据主导地位。尽管C提供了原生的面向对象Object-Oriented Programming, OOP支持但其运行时开销、内存占用及编译器兼容性问题使得多数工业级MCU平台仍以纯C语言作为固件开发的首选。然而这并不意味着嵌入式开发者必须放弃OOP带来的模块化、可复用与可维护性优势。本文基于STM32系列微控制器与HAL库环境详细阐述一种零开销抽象Zero-Cost Abstraction的C语言面向对象实现方法并以I²C总线驱动及其上层AT24C64 EEPROM设备驱动为具体案例完整呈现从底层硬件操作封装到高层设备逻辑抽象的工程化路径。该方案的核心价值不在于炫技或理论堆砌而在于解决嵌入式开发中真实存在的痛点当项目中存在多个I²C外设如温度传感器、加速度计、EEPROM、OLED显示屏时若采用传统函数式编程极易陷入“复制-粘贴-修改引脚定义”的泥潭。每一次新增设备都需重复编写几乎相同的初始化、读写时序代码仅引脚参数不同当硬件发生变更如将EEPROM从GPIOA迁移到GPIOB则需全局搜索并逐一修改所有相关函数调用。这种紧耦合的设计严重阻碍了代码复用与快速迭代。面向对象封装的本质是将“数据属性”与“行为方法”进行逻辑绑定并通过统一接口隐藏实现细节从而在不增加运行时负担的前提下显著提升软件架构的清晰度与鲁棒性。1.1 I²C驱动类的设计与实现I²C是一种典型的主从式、多主多从的串行总线协议其物理层由两条开漏Open-Drain信号线构成SCLSerial Clock和SDASerial Data。协议的健壮性高度依赖于精确的时序控制包括起始条件START、停止条件STOP、应答ACK/NACK以及字节传输。在裸机或HAL库环境下这些操作最终都归结为对GPIO寄存器的位操作与微秒级延时。面向对象封装的第一步是定义一个能完整描述I²C硬件实例的“类”。1.1.1 类结构体定义iic.h在C语言中“类”通过struct实现其成员分为两类属性Attributes和方法指针Method Pointers。属性用于存储该I²C实例所关联的硬件资源信息方法指针则指向具体的实现函数构成该实例的行为契约。// 定义IIC类 typedef struct IIC_Type { // 属性硬件资源映射 GPIO_TypeDef *GPIOx_SCL; // SCL信号线所属的GPIO端口如GPIOA, GPIOB GPIO_TypeDef *GPIOx_SDA; // SDA信号线所属的GPIO端口如GPIOA, GPIOB uint32_t GPIO_SCL; // SCL信号线在端口内的具体引脚号如GPIO_PIN_5 uint32_t GPIO_SDA; // SDA信号线在端口内的具体引脚号如GPIO_PIN_6 // 方法指向具体实现函数的函数指针 void (*IIC_Init)(const struct IIC_Type*); // 初始化GPIO引脚 void (*IIC_Start)(const struct IIC_Type*); // 产生START条件 void (*IIC_Stop)(const struct IIC_Type*); // 产生STOP条件 uint8_t (*IIC_Wait_Ack)(const struct IIC_Type*); // 等待从机应答返回HAL_OK或HAL_ERROR void (*IIC_Ack)(const struct IIC_Type*); // 主机发送ACK信号 void (*IIC_NAck)(const struct IIC_Type*); // 主机发送NACK信号 void (*IIC_Send_Byte)(const struct IIC_Type*, uint8_t); // 发送一个字节 uint8_t (*IIC_Read_Byte)(const struct IIC_Type*, uint8_t); // 读取一个字节ack参数决定是否发送ACK void (*delay_us)(uint32_t); // 微秒级延时函数需用户外部提供 } IIC_TypeDef;此结构体的设计体现了明确的工程目的属性分离将端口GPIOx_SCL/SDA与引脚号GPIO_SCL/SDA分开存储为后续的GPIO寄存器直接操作提供了必要信息避免了在每个方法内部重复解析。方法指针化所有I²C核心操作均被声明为函数指针。这使得同一个结构体定义可以被多个不同的I²C实例如IIC1,IIC2复用每个实例只需在初始化时将自己的方法指针指向各自的具体实现函数即可实现了“一个定义多个实例”的关键目标。延迟解耦delay_us被设计为一个可注入的函数指针而非硬编码的HAL_Delay()或__NOP()循环。这赋予了系统极大的灵活性在调试阶段可使用高精度的DWT周期计数器实现纳秒级延时在低功耗场景下可切换为基于SysTick的低频延时甚至可在仿真环境中注入一个空函数以加速测试。这种设计遵循了依赖倒置原则Dependency Inversion Principle是高质量嵌入式软件架构的基石。1.1.2 类方法的具体实现iic.c方法的实现围绕I²C协议的物理电气特性展开。由于I²C总线要求上拉电阻SCL和SDA在空闲时均为高电平。因此任何输出操作都必须确保引脚配置为推挽输出Push-Pull Output而输入操作如读取SDA状态则必须将引脚配置为浮空输入Floating Input或上拉输入Pull-Up Input以避免总线冲突。// 将SDA引脚配置为输入模式用于读取从机应答 static void SDA_IN(const struct IIC_Type* IIC_Type_t) { uint8_t io_num 0; // 根据GPIO_PIN_x宏的值计算出引脚在MODER寄存器中的位偏移 switch (IIC_Type_t-GPIO_SDA) { case GPIO_PIN_0: io_num 0; break; case GPIO_PIN_1: io_num 1; break; // ... 其他case省略原理相同 case GPIO_PIN_15: io_num 15; break; } // 清除MODER寄存器中对应引脚的2位模式位 IIC_Type_t-GPIOx_SDA-MODER ~(3U (io_num * 2U)); // 设置为输入模式00b IIC_Type_t-GPIOx_SDA-MODER | (0U (io_num * 2U)); } // 将SDA引脚配置为输出模式用于驱动总线 static void SDA_OUT(const struct IIC_Type* IIC_Type_t) { uint8_t io_num 0; // 同上计算位偏移 switch (IIC_Type_t-GPIO_SDA) { // ... 同上 } // 清除MODER寄存器中对应引脚的2位模式位 IIC_Type_t-GPIOx_SDA-MODER ~(3U (io_num * 2U)); // 设置为推挽输出模式01b IIC_Type_t-GPIOx_SDA-MODER | (1U (io_num * 2U)); } // 设置SCL引脚电平 static void IIC_SCL(const struct IIC_Type* IIC_Type_t, int n) { if (n 1) { HAL_GPIO_WritePin(IIC_Type_t-GPIOx_SCL, IIC_Type_t-GPIO_SCL, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(IIC_Type_t-GPIOx_SCL, IIC_Type_t-GPIO_SCL, GPIO_PIN_RESET); } } // 设置SDA引脚电平 static void IIC_SDA(const struct IIC_Type* IIC_Type_t, int n) { if (n 1) { HAL_GPIO_WritePin(IIC_Type_t-GPIOx_SDA, IIC_Type_t-GPIO_SDA, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(IIC_Type_t-GPIOx_SDA, IIC_Type_t-GPIO_SDA, GPIO_PIN_RESET); } } // 读取SDA引脚当前电平 static uint8_t READ_SDA(const struct IIC_Type* IIC_Type_t) { return HAL_GPIO_ReadPin(IIC_Type_t-GPIOx_SDA, IIC_Type_t-GPIO_SDA); }上述底层辅助函数SDA_IN,SDA_OUT,IIC_SCL,IIC_SDA,READ_SDA共同构成了I²C时序操作的原子单元。它们的实现严格遵循了STM32的寄存器操作规范例如通过直接操作MODERMode Register来切换GPIO模式这比调用HAL库的HAL_GPIO_Init()更为高效因为后者包含了大量参数校验与状态管理对于高频、短时的I²C位操作而言是不必要的开销。在此基础上协议级操作得以构建// IIC初始化配置SCL和SDA引脚为推挽输出、上拉、高速 static void IIC_Init_t(const struct IIC_Type* IIC_Type_t) { GPIO_InitTypeDef GPIO_Initure; // 使能对应GPIO端口的时钟根据引脚所属端口动态选择 if (IIC_Type_t-GPIOx_SCL GPIOA || IIC_Type_t-GPIOx_SDA GPIOA) { __HAL_RCC_GPIOA_CLK_ENABLE(); } if (IIC_Type_t-GPIOx_SCL GPIOB || IIC_Type_t-GPIOx_SDA GPIOB) { __HAL_RCC_GPIOB_CLK_ENABLE(); } // ... 其他端口使能逻辑 // 配置SCL引脚 GPIO_Initure.Pin IIC_Type_t-GPIO_SCL; GPIO_Initure.Mode GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_Initure.Pull GPIO_PULLUP; // 必须上拉符合I²C电气规范 GPIO_Initure.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(IIC_Type_t-GPIOx_SCL, GPIO_Initure); // 配置SDA引脚同上 GPIO_Initure.Pin IIC_Type_t-GPIO_SDA; HAL_GPIO_Init(IIC_Type_t-GPIOx_SDA, GPIO_Initure); // 初始化后将SCL和SDA拉高进入空闲状态 IIC_SCL(IIC_Type_t, 1); IIC_SDA(IIC_Type_t, 1); } // IIC Start条件SCL为高时SDA由高变低 static void IIC_Start_t(const struct IIC_Type* IIC_Type_t) { SDA_OUT(IIC_Type_t); // 确保SDA可被驱动 IIC_SDA(IIC_Type_t, 1); IIC_SCL(IIC_Type_t, 1); IIC_Type_t-delay_us(4); // 保持高电平稳定 IIC_SDA(IIC_Type_t, 0); // 产生下降沿 IIC_Type_t-delay_us(4); IIC_SCL(IIC_Type_t, 0); // 钳住总线准备传输 } // IIC Stop条件SCL为高时SDA由低变高 static void IIC_Stop_t(const struct IIC_Type* IIC_Type_t) { SDA_OUT(IIC_Type_t); IIC_SCL(IIC_Type_t, 0); IIC_SDA(IIC_Type_t, 0); IIC_Type_t-delay_us(4); IIC_SCL(IIC_Type_t, 1); IIC_SDA(IIC_Type_t, 1); IIC_Type_t-delay_us(4); } // 等待从机应答主机释放SDA设为输入然后拉高SCL读取SDA电平 static uint8_t IIC_Wait_Ack_t(const struct IIC_Type* IIC_Type_t) { uint8_t ucErrTime 0; SDA_IN(IIC_Type_t); // SDA设为输入让从机驱动 IIC_SDA(IIC_Type_t, 1); IIC_Type_t-delay_us(1); IIC_SCL(IIC_Type_t, 1); IIC_Type_t-delay_us(1); while (READ_SDA(IIC_Type_t)) { // 等待SDA被从机拉低 ucErrTime; if (ucErrTime 250) { // 超时处理 IIC_Type_t-IIC_Stop(IIC_Type_t); return HAL_ERROR; } } IIC_SCL(IIC_Type_t, 0); return HAL_OK; }整个实现过程的关键工程考量在于时序的精确性与鲁棒性。例如在IIC_Wait_Ack_t中超时机制ucErrTime 250是必不可少的它防止了因从机故障或总线短路导致的程序死锁。而delay_us的调用位置与时间长度则直接决定了I²C通信能否在特定速率如100kHz标准模式或400kHz快速模式下稳定工作。这些细节正是将一个“能用”的驱动打磨成一个“可靠”的驱动的核心所在。1.1.3 类实例的创建与使用面向对象的精髓在于“实例化”。一个类定义只是蓝图只有创建了具体的实例才能与真实的硬件交互。在C语言中这通过声明一个struct变量并为其所有方法指针赋值来完成。// 实例化一个IIC1外设相当于一个结构体变量 IIC_TypeDef IIC1 { .GPIOx_SCL GPIOA, .GPIOx_SDA GPIOA, .GPIO_SCL GPIO_PIN_5, .GPIO_SDA GPIO_PIN_6, .IIC_Init IIC_Init_t, .IIC_Start IIC_Start_t, .IIC_Stop IIC_Stop_t, .IIC_Wait_Ack IIC_Wait_Ack_t, .IIC_Ack IIC_Ack_t, .IIC_NAck IIC_NAck_t, .IIC_Send_Byte IIC_Send_Byte_t, .IIC_Read_Byte IIC_Read_Byte_t, .delay_us delay_us // 此函数需在其他文件中实现 };这个IIC1变量就是一个完整的、可立即使用的I²C总线控制器对象。它的所有属性引脚和方法函数指针都在编译期被确定运行时无需任何额外的内存分配或虚函数表vtable查找真正实现了零开销。开发者在后续代码中只需调用IIC1.IIC_Init(IIC1)即可完成初始化调用IIC1.IIC_Start(IIC1)即可发起一次通信语法简洁语义清晰且完全隔离了底层寄存器操作的复杂性。2. 基于组合关系的AT24C64 EEPROM设备驱动封装I²C总线本身只是一个通信通道其价值在于连接各种智能外设。AT24C64是一款容量为64Kbit8KB的串行EEPROM它遵循I²C协议但拥有自己独特的寻址方式、页写入限制和写入时序要求。将AT24C64的驱动也进行面向对象封装是展示OOP在嵌入式分层架构中强大威力的绝佳范例。2.1 设备类的设计哲学组合优于继承在面向对象设计中继承Inheritance和组合Composition是两种基本的代码复用方式。初学者常倾向于使用继承认为“AT24C64是一种I²C设备”故应让AT24CXX_Type继承自IIC_Type。然而这是一种概念上的误用。继承表达的是“is-a”是一个关系适用于具有强共性的类型体系如Vehicle-Car-ElectricCar。而AT24C64与I²C总线之间是典型的“has-a”有一个关系一个AT24C64设备拥有一个I²C接口来与之通信。它并非I²C总线本身也不共享I²C总线的任何属性如SCL/SDA引脚。因此正确的设计是组合在AT24CXX_Type结构体中将一个IIC_TypeDef类型的成员作为其组成部分。这种方式不仅在语义上更准确而且在工程上更具优势松耦合AT24C64的实现完全依赖于I²C的公共接口即IIC_TypeDef结构体中定义的方法而不关心I²C的具体实现细节。这意味着未来如果需要将AT24C64迁移到一个基于硬件I²C外设而非软件模拟的平台上只需重新实现一个IIC_Hardware类并将其注入到AT24CXX_Type中上层业务代码无需做任何修改。单一职责IIC_TypeDef只负责总线时序AT24CXX_TypeDef只负责EEPROM的存储逻辑职责边界清晰便于独立测试与维护。灵活性一个AT24CXX_TypeDef实例可以轻松地与任意一个已有的IIC_TypeDef实例如IIC1,IIC2绑定实现硬件资源的灵活调度。2.1.1 AT24CXX设备类定义at24cxx.h// 定义AT24CXX存储器容量常量 #define AT24C01 127 #define AT24C02 255 #define AT24C04 511 #define AT24C08 1023 #define AT24C16 2047 #define AT24C32 4095 #define AT24C64 8191 // 8KB #define AT24C128 16383 #define AT24C256 32767 // 定义AT24CXX类 typedef struct AT24CXX_Type { // 属性 uint32_t EEP_TYPE; // 存储器类型容量用于区分不同型号的地址空间 // 组合包含一个IIC类实例 IIC_TypeDef IIC; // 方法AT24CXX特有的设备级操作 uint8_t (*AT24CXX_ReadOneByte)(const struct AT24CXX_Type*, uint16_t); void (*AT24CXX_WriteOneByte)(const struct AT24CXX_Type*, uint16_t, uint8_t); void (*AT24CXX_WriteLenByte)(uint16_t, uint32_t, uint8_t); uint32_t (*AT24CXX_ReadLenByte)(uint16_t, uint8_t); void (*AT24CXX_Write)(uint16_t, uint8_t*, uint16_t); void (*AT24CXX_Read)(uint16_t, uint8_t*, uint16_t); void (*AT24CXX_Init)(const struct AT24CXX_Type*); uint8_t (*AT24CXX_Check)(const struct AT24CXX_Type*); } AT24CXX_TypeDef; extern AT24CXX_TypeDef AT24C_64; // 外部声明供其他文件使用此定义清晰地展现了组合模式IIC_TypeDef IIC;这一行就是AT24C64与I²C总线之间的桥梁。所有对EEPROM的读写操作最终都将通过调用IIC成员的IIC_Send_Byte、IIC_Read_Byte等方法来完成。2.1.2 设备类方法的实现at24cxx.cAT24C64的I²C从机地址由硬件引脚A2/A1/A0决定其默认地址为0x50二进制1010000。在I²C通信中地址的最低位R/W位用于指示读写方向写操作为0x500b10100000读操作为0x510b10100001。此外AT24C64的地址空间为13位0x0000 - 0x1FFF这意味着在进行随机读写时需要发送两个字节的地址高字节、低字节。// 在AT24CXX指定地址读出一个数据 static uint8_t AT24CXX_ReadOneByte_t(const struct AT24CXX_Type* AT24CXX_Type_t, uint16_t ReadAddr) { uint8_t temp 0; // 第一步发送START条件 AT24CXX_Type_t-IIC.IIC_Start(AT24CXX_Type_t-IIC); // 第二步发送器件地址写命令0xA0进入写地址模式 // 对于大于AT24C16的型号地址空间2047需要发送16位地址 if (AT24CXX_Type_t-EEP_TYPE AT24C16) { AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, 0xA0); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); // 发送16位地址的高字节 AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, ReadAddr 8); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); } else { // 对于小容量型号地址为8位通过A2/A1/A0引脚扩展 // 计算地址0xA0 ((地址高5位) 1) AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, 0xA0 ((ReadAddr / 256) 1)); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); } // 第三步发送16位地址的低字节 AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, ReadAddr % 256); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); // 第四步再次发送START条件切换到读模式 AT24CXX_Type_t-IIC.IIC_Start(AT24CXX_Type_t-IIC); // 第五步发送器件地址读命令0xA1 AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, 0xA1); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); // 第六步读取一个字节并发送NACK表示读取结束 temp AT24CXX_Type_t-IIC.IIC_Read_Byte(AT24CXX_Type_t-IIC, 0); AT24CXX_Type_t-IIC.IIC_Stop(AT24CXX_Type_t-IIC); return temp; } // 在AT24CXX指定地址写入一个数据 static void AT24CXX_WriteOneByte_t(const struct AT24CXX_Type* AT24CXX_Type_t, uint16_t WriteAddr, uint8_t DataToWrite) { // 重复上述写地址流程... AT24CXX_Type_t-IIC.IIC_Start(AT24CXX_Type_t-IIC); if (AT24CXX_Type_t-EEP_TYPE AT24C16) { AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, 0xA0); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, WriteAddr 8); } else { AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, 0xA0 ((WriteAddr / 256) 1)); } AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, WriteAddr % 256); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); // 发送要写入的数据字节 AT24CXX_Type_t-IIC.IIC_Send_Byte(AT24CXX_Type_t-IIC, DataToWrite); AT24CXX_Type_t-IIC.IIC_Wait_Ack(AT24CXX_Type_t-IIC); AT24CXX_Type_t-IIC.IIC_Stop(AT24CXX_Type_t-IIC); // 关键写入后必须等待EEPROM内部写入完成典型值10ms AT24CXX_Type_t-IIC.delay_us(10000); }AT24CXX_WriteOneByte_t末尾的delay_us(10000)是AT24C64数据手册中明确规定的写入周期时间Write Cycle Time。这是一个至关重要的工程细节。如果省略此延时紧接着的下一次读写操作将无法获取到刚写入的数据因为EEPROM芯片内部仍在执行擦除与编程操作。面向对象封装的价值在此刻凸显这个延时被封装在了设备驱动内部对上层应用完全透明。应用开发者只需调用AT24C_64.AT24CXX_WriteOneByte(AT24C_64, addr, data)无需记忆任何关于EEPROM硬件特性的知识。2.1.3 设备类实例的创建与I²C类类似AT24C64的实例化同样是一个静态结构体声明但它将I²C的实例作为其内部成员进行初始化。// 实例化AT24C64对象 AT24CXX_TypeDef AT24C_64 { .EEP_TYPE AT24C64, .IIC { .GPIOx_SCL GPIOA, .GPIOx_SDA GPIOA, .GPIO_SCL GPIO_PIN_5, .GPIO_SDA GPIO_PIN_6, .IIC_Init IIC_Init_t, .IIC_Start IIC_Start_t, .IIC_Stop IIC_Stop_t, .IIC_Wait_Ack IIC_Wait_Ack_t, .IIC_Ack IIC_Ack_t, .IIC_NAck IIC_NAck_t, .IIC_Send_Byte IIC_Send_Byte_t, .IIC_Read_Byte IIC_Read_Byte_t, .delay_us delay_us }, .AT24CXX_ReadOneByte AT24CXX_ReadOneByte_t, .AT24CXX_WriteOneByte AT24CXX_WriteOneByte_t, // ... 其他方法指针初始化 .AT24CXX_Init AT24CXX_Init_t, .AT24CXX_Check AT24CXX_Check_t };这个声明的精妙之处在于它将I²C的硬件配置引脚与AT24C64的设备逻辑读写方法完美地融合在一个对象中。开发者在使用时只需关注AT24C_64这一个名字其背后复杂的硬件交互与协议细节已被彻底封装。3. 应用层集成与系统级验证面向对象封装的最终价值必须在实际的应用场景中得到体现。一个设计精良的驱动其API应当足够简洁以至于主程序的逻辑可以像阅读自然语言一样清晰。3.1 主函数中的设备使用范例以下是一个典型的main()函数片段展示了如何利用已封装好的AT24C_64对象进行初始化、自检与数据操作。#include at24cxx.h int main(void) { // 省略系统时钟、SysTick、串口等基础外设的初始化... // 第一步调用对象的初始化方法 AT24C_64.AT24CXX_Init(AT24C_64); // 第二步调用对象的自检方法验证硬件连接与通信链路 if (AT24C_64.AT24CXX_Check(AT24C_64) 0) { printf(AT24C64检测成功\r\n); } else { printf(AT24C64检测失败\r\n); // 可在此处加入错误处理如点亮LED、进入死循环等 while(1); } // 第三步进行实际的数据读写操作 uint16_t test_addr 0x0000; uint8_t write_data 0xAA; uint8_t read_data 0; // 写入一个字节 AT24C_64.AT24CXX_WriteOneByte(AT24C_64, test_addr, write_data); // 短暂延时确保写入完成虽然驱动内部已有10ms延时此处为保险 HAL_Delay(15); // 读取该字节进行验证 read_data AT24C_64.AT24CXX_ReadOneByte(AT24C_64, test_addr); if (read_data write_data) { printf(AT24C64读写测试成功写入: 0x%02X, 读取: 0x%02X\r\n, write_data, read_data); } else { printf(AT24C64读写测试失败\r\n); } // 第四步进行批量数据操作 uint8_t tx_buffer[16] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}; uint8_t rx_buffer[16] {0}; // 向地址0x0010开始写入16个字节 AT24C_64.AT24CXX_Write(AT24C_64, 0x0010, tx_buffer, 16); // 从地址0x0010开始读取16个字节 AT24C_64.AT24CXX_Read(AT24C_64, 0x0010, rx_buffer, 16); // 验证数据一致性 for (uint8_t i 0; i 16; i) { if (tx_buffer[i] ! rx_buffer[i]) { printf(批量读写校验失败位置 %d\r\n, i); break; } } printf(批量读写测试完成。\r\n); while (1) { // 主循环 } }这段代码的可读性与可维护性是传统函数式编程难以企及的。所有的操作都围绕着AT24C_64这个单一对象展开其方法名AT24CXX_Init,AT24CXX_Check,AT24CXX_WriteOneByte直白地表达了意图无需查阅任何文档即可理解。更重要的是它完美地实现了关注点分离Separation of Concernsmain()函数只负责业务逻辑“我要做什么”而所有与硬件交互的细节“我该如何做”都被下沉到了at24cxx.c和iic.c中。这种清晰的分层是构建大型、可演进嵌入式系统的基础。3.2 封装带来的工程收益总结通过对I²C驱动与AT24C64设备驱动的面向对象重构我们获得了以下切实的工程收益收益维度传统函数式编程面向对象封装代码复用性每新增一个I²C设备需复制一份引脚配置和大部分时序代码。只需声明一个新的IIC_TypeDef或AT24CXX_TypeDef实例复用全部逻辑。可维护性修改I²C时序如调整延时需在所有相关函数中逐一查找并修改。只需修改iic.c中对应的delay_us调用或其实现所有实例自动受益。可测试性难以对I²C底层进行单元测试因为其与HAL库深度耦合。IIC_TypeDef
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2436127.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!