当前位置: 首页 > news >正文

HAL 库设置回调成员函数的一种方法

HAL 库都是拿C 写的,想注册回调函数的话,也只能是C 的函数,不能用成员函数作为回调。现在有个需求,要给一个I2C 设备写个驱动,希望把驱动整体封装成个类,这样比较灵活。要是还得把I2C 的回调函数放在类外面,那就太不“整洁”了。此外,放在外面的回调函数没办法直接引用到设备驱动对象,必须有个全局的指针变量,让它指向驱动对象,然后回调函数里再使用这个全局的指针去找对象。STM32DUINO 框架里就是这么设计的,只是它们稍微取了点巧,大致原理如下:

// 驱动类,或者驱动结构体
struct Driver{
	I2C_HandleTypeDef handle;
	// ... 其他一堆成员变量
	
	// ... 其他一堆成员函数
};

// 驱动对象
Driver driver_instance;

// 调用HAL 库函数
HAL_I2C_Master_Transmit_DMA(&driver_instance.handle, addr, buf_ptr, count);

// HAL 库的回调函数。想在回调里使用C++ 代码,必须把回调放在CPP 文件里,所以要加上extern "C"
extern "C" {

void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *I2cHandle) {
	// 根据handle 结构体在Driver 类中的偏移量,拿到指向driver_instance 对象的指针
  auto offset = offsetof(Driver, handle);
  Driver *driver_ptr = reinterpret_cast<Driver*>(I2cHandle - offset);
  // 拿到了驱动对象,之后就随便弄了
  // driver_ptr->.......
}

}

把HAL 库的Handle 结构体嵌进驱动对象里,再用这个handle 成员变量去调用各种HAL 函数,那么回调函数传进来的*I2cHandle 当然就是指向这个成员变量的指针。有了成员变量的地址,就可以根据成员变量在驱动对象中的偏移量,拿到驱动对象的地址。也是挺巧妙的,优点是不用单独定义个隐藏的全局变量,用户使用的时候会自己定义Driver 对象。

不过这种做法不适合我。我习惯把Cube 生成的代码简单改改就直接用,而且还是拿脚本自动改。Cube 会在它生成的main.c 里定义好各种handle 结构,我就在它的main 函数里调用我自己的入口函数app()。这样做的好处是可以分离Cube 自动生成的代码,我可以把我的代码放在app.cpp 文件里,以后如果改了配置,只要把Cube 新生成的代码复制过来改一下就好了,我可不想在它生成的代码里照着它规定好的格式填空。

于是,既然handle 都已经在main.c 里定义好了,我肯定不想一个一个自己定义Driver 对象,再去改生成好的配置代码。

所以我用的是另一种比较脏的方法,就是直接修改HAL 库的头文件,在I2C_HandleTypeDef 结构体定义里加一个成员void *PtrToCallbackObject,用来指向我的驱动对象。

typedef struct __I2C_HandleTypeDef
#else
typedef struct
#endif  /* USE_HAL_I2C_REGISTER_CALLBACKS */
{
  I2C_TypeDef                *Instance;      /*!< I2C registers base address               */

// ...


#if (USE_HAL_I2C_REGISTER_CALLBACKS == 1)
  
  void *PtrToCallbackObject;   // 指向回调函数关联的对象

// ...

  void (* AddrCallback)(struct __I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode);  /*!< I2C Slave Address Match callback */

  void (* MspInitCallback)(struct __I2C_HandleTypeDef *hi2c);                /*!< I2C Msp Init callback                     */
  void (* MspDeInitCallback)(struct __I2C_HandleTypeDef *hi2c);              /*!< I2C Msp DeInit callback                   */

#endif  /* USE_HAL_I2C_REGISTER_CALLBACKS */
} I2C_HandleTypeDef;

// 回调
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *I2cHandle) {
	// 直接拿到驱动对象的指针
  Driver *driver_ptr = reinterpret_cast<Driver*>(I2cHandle.PtrToCallbackObject);
  // driver_ptr->.......
}

这样只是改了头文件的一行代码,几乎不会引起任何兼容性问题。虽然如果HAL 库要更新,那就得重新改,但是相信大伙很多人用的库都是复制粘贴祖传下来的,根本不知道版本是什么。而且就算更新以后忘记改了,代码直接不能编译,会提示你缺个PtrToCallbackObject 成员,所以不会出错。最后,这个Handle 结构体里面已经定义了这么多不知道有没有用的变量,我再加一个指针,不会造成什么负担。此外,就算不用C++,在C 里面往往也会用结构体模拟对象,增加这个成员也是有用的。

有了这个方便的修改以后,回到开头,现在可以把回调函数放在驱动类里面了:


class Driver {
   private:
   I2C_HandleTypeDef _handle;  // 指向设备要使用的I2C handle
	
   public:
   Driver(I2C_HandleTypeDef h) : _handle(h) {}

	 void init() {
	       // 注册回调函数,先把对象的指针放进去
	       _handle->PtrToCallbackObject = reinterpret_cast<void *>(this);
	
	       // 用这些lambda 作为回调函数。捕获列表必须为空,否则不能作为普通函数指针使用,
	       // 所以lambda 里不能直接操作this 指针,必须从handle 里获取。
	       // WARNING: 注意,这些回调是从I2C 中断调用的
	
	       // TX 完成回调
	       auto master_tx_complete_callback = [](I2C_HandleTypeDef *h) {
	           // 获取this 指针
	           auto t = reinterpret_cast<Driver *>(h->PtrToCallbackObject);
	           // 发送完成时,调用成员函数
	           t->on_tx_complete();
	       };
	
	       // 错误回调
	       auto error_callback = [](I2C_HandleTypeDef *h) {
	           auto t = reinterpret_cast<Driver *>(h->PtrToCallbackObject);
	           // 发生错误时
	           t->on_error();
	       };
				
				// 注册回调函数
				_handle.MasterTxCpltCallback = master_tx_complete_callback;
				_handle.ErrorCallback = error_callback;
	   }
};

这样有什么优点?可以把驱动代码全部放在头文件里,用的时候include 一下就行了,多方便。

相关文章:

  • 2-vim编辑器的安装和使用
  • 【爬虫】携程旅游项目数据爬取
  • GPT-4o从语义分割到深度图生成,大模型狂潮下的计算机视觉:技术进步≠替代危机
  • C#UDP协议客户端工具类
  • C#实现存储数据到Redis
  • 运行小程序报错
  • Leetcode 3508. Implement Router
  • Java数据结构的基础用法
  • 嵌入式AI开源生态指南:从框架到应用的全面解析
  • JavaScript学习教程,从入门到精通,JavaScript 基础语法全面指南(5)
  • 2025.4.6总结
  • 低空经济基础设施建设方向与展望
  • 深入详解流形学习中的几何解释
  • 机器学习/深度学习
  • HTML语言的数据可视化
  • deepseek为采用JAVA重构模型运营平台vLLM和SGLang指定的计划
  • 【学习笔记】Ruckig: 高效实时运动规划库
  • 如何获取oracle cloud永久免费的vps(4C/24G)?
  • 机器学习的一百个概念(10)假阳性率
  • Spring 中的 bean 生命周期
  • 网站建设规划ppt/常用的关键词优化策略有哪些
  • 移除wordpress4版本号/seo推广编辑
  • 响应式环保网站模板下载/培训机构优化
  • 怎么破解网站后台/关键词app
  • 网站建设有那些/百度小说排行榜前十名
  • 哪家公司做网站不错/百度com打开