用C++ 包装STM32 官方固件库 - 链式调用改写初始化结构体

news2024/11/25 13:00:28

拿C++ 在固件库上套娃一层有几点原因:

  1. 固件库都是用C 写的,而我平时都用C++,虽然是兼容的,但C 的一些特性我不喜欢;
  2. 我不喜欢官方库的函数命名风格;
  3. 各个厂家的固件库大同小异,但是“小异”的那一部分很烦人,比如几个函数和常量命名不同,包装一层可以隔离这些差异;
  4. 我不喜欢固件库里惯用的初始化结构体,用着麻烦;

以下就简单的介绍一下思路,以定时器TIM 操作函数为例。

链式调用初始化

首先考虑初始化结构体的改进方案,比如时基初始化结构体:

typedef struct
{
  uint16_t TIM_Prescaler;         /*!< Specifies the prescaler value used to divide the TIM clock.
                                       This parameter can be a number between 0x0000 and 0xFFFF */
  uint16_t TIM_CounterMode;       /*!< Specifies the counter mode.
                                       This parameter can be a value of @ref TIM_Counter_Mode */
  uint16_t TIM_Period;            /*!< Specifies the period value to be loaded into the active
                                       Auto-Reload Register at the next update event.
                                       This parameter must be a number between 0x0000 and 0xFFFF.  */ 
  uint16_t TIM_ClockDivision;     /*!< Specifies the clock division.
                                      This parameter can be a value of @ref TIM_Clock_Division_CKD */
  uint8_t TIM_RepetitionCounter;  /*!< Specifies the repetition counter value. Each time the RCR downcounter
                                       reaches zero, an update event is generated and counting restarts
                                       from the RCR value (N).
                                       This means in PWM mode that (N+1) corresponds to:
                                          - the number of PWM periods in edge-aligned mode
                                          - the number of half PWM period in center-aligned mode
                                       This parameter must be a number between 0x00 and 0xFF. 
                                       @note This parameter is valid only for TIM1 and TIM8. */
} TIM_TimeBaseInitTypeDef;       

参数挺多的,而且都是整数类型,如果简单的把这些参数都当作函数参数传进一个初始化函数里,可读性会很差,可能类似下面这样:

init_time_base(1000, TIM_CounterMode_Up, 5000, 1, 0);

阅读者无法一眼看出哪个参数对应哪个功能,必须熟悉函数的参数顺序。这时候就会羡慕python 里有命名参数,调用函数时必须手写上参数名,参数功能当然一看便知。而C++ 里处理这个问题,除了用结构体传参数,还有两、三种方法,参考:Design Patterns With C++(九)命名参数与方法链。给函数加默认参数只能少写一两个参数,解决不了可读性问题,所以决定采用链式调用风格。

先看一下包装之后的用法:

void main() {
	// ...
	using namespace timxx;
	
	// 初始化TIM2 TIM3 时基
	TimeBaseInit()
		.division(clock_div::div1)
		.mode(counter_mode::up)
		.prescaler(1000)
		.period(5000)
		.init(TIM2)    // 这一步才实际调用库函数执行初始化,其他几个函数都是在给内部结构体赋值
		.prescaler(2000)
		.period(2000)
		.init(TIM3);

	// ...
}

对比原来的库代码,书写简洁程度上见仁见智吧,反正不用手动创建个结构体,解决了我的核心痛点,变量和函数命名比库代码简洁,因为不用担心重名。原理很简单,就是写一个名叫TimeBaseInit 的类,把初始化结构体藏在里面,使用时不必把对象赋值给一个变量,随用随创建,用完就把对象销毁了。这个类的所有成员函数都会返回该对象的引用,所以函数调用后还可以继续接着链式调用,每次调用的都是那个匿名对象。类里面只有init 函数实际去执行初始化步骤,其他函数都只是给内部结构体赋值。

在继续之前,先说明一下,当然,这么包一层肯定会付出一些代价,包括运行时间和空间占用,后面会有编译结果比较。有官方库“珠玉在前”,我觉得相比之下,这层包装付出的代价并不显著,有兴趣可以看看常用的GPIO 初始化函数在固件库里是怎么实现的,可以说是资源浪费的典范[doge]。另一方面,固件库里都是C 函数,函数实现都分开放在.c 文件里,编译器内联的可能性应该不大,因此就算是简单的给引脚设置个电平,用固件库也会产生额外的函数调用,所以很多人是在宏里自己写寄存器操作的。用C++ 的话,这种简单的函数放在头文件里,编译后就内联了,资源使用上和宏没区别。

初始化实现

上面的代码中用到了两个枚举: clock_divcounter_mode,先把这俩写出来:

/**
 * @brief 计数模式
 *
 */
enum class counter_mode : decltype(TIM_CounterMode_Up) {
    up = TIM_CounterMode_Up,  // 递增后归零。基本定时器只支持向上模式,所以初始化默认值为向上
    down = TIM_CounterMode_Down,
    center_1 = TIM_CounterMode_CenterAligned1,
    center_2 = TIM_CounterMode_CenterAligned2,
    center_3 = TIM_CounterMode_CenterAligned3,
};

enum class clock_div: decltype(TIM_CKD_DIV1) {
    div1 = TIM_CKD_DIV1,
    div2 = TIM_CKD_DIV2,
    div4 = TIM_CKD_DIV4,
};

就是用枚举把固件库里常量值包装了一下,优点是这样一来就不用在函数里做参数检查了,只有这些值能传进去。decltype(TIM_CounterMode_Up) 用来获取常量值的数据类型,让枚举得底层类型和这些常量一致。然后是那个类的代码:

class TimeBaseInit {
   private:
    // TODO: 默认的初始化参数为:向上计数、
    TIM_TimeBaseInitTypeDef _init_struct = {
        .TIM_Prescaler = 0,
        .TIM_CounterMode = _ENUM_TO_UNDERLYING(counter_mode::up),
        .TIM_Period = 0,
        .TIM_ClockDivision = TIM_CKD_DIV1,
        .TIM_RepetitionCounter = 0};

   public:
    TimeBaseInit& division(clock_div clkdiv) {
        _init_struct.TIM_ClockDivision = _ENUM_TO_UNDERLYING(clkdiv);
        return *this;
    }

    TimeBaseInit& prescaler(uint16_t prs) {
        _init_struct.TIM_Prescaler = prs;
        return *this;
    }

    TimeBaseInit& period(uint16_t prd) {
        _init_struct.TIM_Period = prd;
        return *this;
    }

    TimeBaseInit& mode(counter_mode cm) {
        _init_struct.TIM_CounterMode = _ENUM_TO_UNDERLYING(cm);
        return *this;
    }

    TimeBaseInit& repetition_counter(uint8_t c) {
        _init_struct.TIM_RepetitionCounter = c;
        return *this;
    }

    TimeBaseInit& init(TIM_TypeDef* tim_x) {
        TIM_TimeBaseInit(tim_x, &_init_struct);
        return *this;
    }
};

就是像上面说的,类里有个私有的成员变量,即初始化结构体。对象创建时,会先用默认参数初始化这个结构体。默认参数是比较常用的值,没必要不用改,之后调用初始化的时候可以少写一两行代码。顺便一说,这样初始化应该不会有额外的代价,按我的理解,结构体创建出来后,内部成员本来就要初始化为0,这样写只是把默认的0 改成了别的值,不会重复生成初始化赋值的代码。

除了最后的init,其他函数都只是给结构体赋一个值,编译器应该可以内联掉,不会付出调用函数的开销。赋值时调用了一个宏,作用是把枚举转换成对应的底层类型,在这里就是转换成了uint16_t,然后才能送进结构体里。宏的内容如下:

#include <type_traits>

#define _ENUM_TO_UNDERLYING(e) static_cast<std::underlying_type_t<decltype(e)>>(e)

用到了C++ 标准库的<type_traits>

编译结果对比

总之也没什么好说的,对比一下执行同样功能时,直接用固件库和用包装的存储占用,比较的代码如下:

// C++ 包装
    TimeBaseInit()
        .division(timxx::clock_div::div1)
        .mode(timxx::counter_mode::up)
        .prescaler(1000)
        .period(1000)
        .repetition_counter(0)
        .init(TIM1)
        .prescaler(2000)
        .period(1000)
        .init(TIM2);

// 直接用固件库
    TIM_TimeBaseInitTypeDef init_struct;
    init_struct.TIM_ClockDivision = TIM_CKD_DIV1;
    init_struct.TIM_CounterMode = TIM_CounterMode_Up;
    init_struct.TIM_Period = 1000;
    init_struct.TIM_Prescaler = 1000;
    init_struct.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM1, &init_struct);
    init_struct.TIM_Period = 2000;
    init_struct.TIM_Prescaler = 2000;
    TIM_TimeBaseInit(TIM12, &init_struct);

环境是PlatformIO,编译器arm-none-eabi-gcc-7.2.1,优化等级Os。先是用C++ 包装的编译结果:

在这里插入图片描述

Flash 总占用6508 字节,包括了项目里别的代码,不是只有上面那段。然后是用固件库的编译结果:

在这里插入图片描述
占用6500 字节,只少了8 个字节。STM32 是32 位架构,所以8 个字节只是两个字。显然,C++ 包装代码里那些函数调用大部分应该是被优化掉了,调用函数给结构体赋值和直接赋值差不多。顺便再对比一下没有这些代码时的体积:

在这里插入图片描述

可见,哪怕是直接用固件库,也产生了196 字节的占用,C++ 版本多占了4%,就是个零头。

其他函数

解决了初始化函数这个重难点,其他固件库函数就很简单了,遇到常量参数就全写进一个枚举里面,然后写一层简单的C++ 函数把原来的库函数包起来,或者就不用库了,把库代码复制粘贴过来,直接操作寄存器。简单的封装函数和寄存器操作函数就放在头文件里,加上inline 属性,编译后可以内联,消除调用开销。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/599089.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

路径规划算法:基于旗鱼优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于旗鱼优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于旗鱼优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法旗鱼…

Direct3D 12——几何——基础

在几何里可分为两种几何&#xff0c;一种是显式几何&#xff0c;另外一种是隐式几何。有不同的方式表示不同的几何 隐式几何 隐式实际上是说不会告诉具体的这些点点就在哪&#xff0c;只告诉你这些点满足的关系。表示一定的关系但并不会给实际的点 例子&#xff1a; 隐式几何…

Ubuntu开机桌面黑屏只有鼠标问题解决办法(搜狗输入法导致)

参考&#xff1a; Ubuntu开机桌面黑屏只有鼠标问题解决办法&#xff08;搜狗输入法导致&#xff09; 问题描述 笔者在安装完搜狗输入法重启电脑后&#xff0c;电脑开机黑屏&#xff0c;只有鼠标的光标可以移动。笔者一开始以为是系统问题&#xff0c;网上查阅资料才发现有大量…

华为OD机试之全量和已占用字符集(Java源码)

全量和已占用字符集 题目描述 给定两个字符集合&#xff0c;一个是全量字符集&#xff0c;一个是已占用字符集&#xff0c;已占用字符集中的字符不能再使用。 输入描述 输入一个字符串 一定包含&#xff0c;前为全量字符集 后的为已占用字符集已占用字符集中的字符一定是全量字…

安全架构审计

安全架构审计 目录概述需求&#xff1a; 设计思路实现思路分析1.什么是安全架构审计2.安全架构审计工具3.现在使用的工具4.安全架构审计报告 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;ski…

最全Python+Selenium环境搭建教程-你绝对想不到有这么简单!

一&#xff0c;Selenium 简介 在华为工作了10年的大佬出的Web自动化测试教程&#xff0c;华为现用技术教程&#xff01;_哔哩哔哩_bilibili在华为工作了10年的大佬出的Web自动化测试教程&#xff0c;华为现用技术教程&#xff01;共计16条视频&#xff0c;包括&#xff1a;1. …

力扣动态规划专题(一)509 70 746 62 63 343 96 思路及C++实现

文章目录 动态规划509. 斐波那契数五步骤代码 70. 爬楼梯五步骤代码 746. 使用最小花费爬楼梯五步骤代码扩展 62. 不同路径动态规划数论 63. 不同路径 II五步骤代码 343. 整数拆分五步骤代码 96.不同的二叉搜索树五步骤代码 注意点&#xff1a; 动态规划 动态规划&#xff0c;…

Java多线程学习2

1. 多线程 线程与任务的关系 脱离了任务的线程是没有意义的 线程对象是通过Thread类来创建的 任务是通过Runnable接口来定义的 1.继承Thread类 2.实现Runnable接口 3.实现Callable接口 &#xff08;与Runnable的区别&#xff0c;可以拿到返回值&#xff09; Thread线程…

RK3588平台开发系列讲解(驱动基础篇)设备驱动 IO 控制

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们来讲的 ioctl 接口。 在内核 3.0 以前,ioctl 接口的名字叫 ioctl;内核 3.0 以后,ioctl 接口的名字叫 unlocked_ioctl。unlocked_ioctl就是 ioctl 接…

基于深度学习的高精度血小板检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度血小板检测&#xff08;红细胞RBC、白细胞WBC和血小板Platelet&#xff09;识别系统可用于日常生活中或野外来检测与定位血小板目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的血小板目标检测识别&#xff0c;另外支持…

程序设计综合实习(C语言):用指针优化学生成绩排名

一.目的 1&#xff0e;熟悉变量的指针和指向变量的的指针变量的概念和使用 2&#xff0e;熟悉数组的指针和指向数组的的指针变量的概念和使用 3. 掌握冒泡法或选择法排序的算法 4. 掌握函数的定义、调用、声明&#xff0c;以及参数的两种传递方式 二、实习环境 Visual Studio 2…

模拟集成电路设计-MOS器件物理基础(模集系列持续更新)

学习目的 欠缺的学习路径&#xff1a; 固体物理&#xff0c;半导体器件物理&#xff0c;器件模型&#xff0c;电路设计。所有的半导体器件都看成一个黑盒子&#xff0c;只关注端电压电流。 最佳的学习路径&#xff1a;两手抓&#xff0c;因为有些二阶效应会影响到电路本身. 本…

Vector-常用CAN工具 - 以太网工程更换环境无法打开解决

通常来说每个VN5000都有自己本身的以太网硬件网络配置&#xff0c;因此当我们打开别人的以太网CANoe工程或CANape工程的时候&#xff0c;无法进行正常的功能使用或者log的无法正常的显示&#xff0c;那我们该如何处理呢&#xff1f;这种情况常见的有以下2种可能&#xff1a; 1、…

数据的表示与运算

目录 一、进位计数制 二、信息编码 三、定点数数据表示 四、校验码 五、定点数补码加减运算 六、标志位的生成 七、定点数的移位运算 八、定点数的乘除运算 九、浮点数的表示 十、浮点数的运算 一、进位计数制 整数部分&#xff1a; 二进制、八进制、十六进制 --…

机器人模型预测控制MPC(model predictive control)

当前控制动作是在每一个采样瞬间通过求解一个有限时域开环最优控制问题而获得。过程的当前状态作为最优控制问题的初始状态&#xff0c;解得的最优控制序列只实施第一个控制作用。这是它与那些使用预先计算控制律的算法的最大不同。本质上模型预测控制求解一个开环最优控制问题…

使用APIPOST 进行压力测试

使用APIPOST 进行压力测试 目录概述需求&#xff1a; 设计思路实现思路分析1.apipost 压力测试 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for c…

​【指针和数组区别与理解】超万字

指针和数组的关系 指针指的是指针变量&#xff0c;不是数组&#xff0c;指针变量的大小是4/8个字节&#xff0c;是专门来存放地址的。数组也不是指针&#xff0c;数组是一块连续的空间&#xff0c;存放一组相同类型的数据的。 没有关系&#xff0c;但是它们之间有比较相似的地方…

代理模式的学习与使用

1、代理模式的学习 代理模式是一种结构型设计模式&#xff0c;它允许你提供一个代理对象&#xff0c;该对象可以控制对其他对象的访问。代理模式通过在代理对象和实际对象之间添加一个中间层&#xff0c;使得代理对象可以代表实际对象执行某些操作&#xff0c;从而实现对实际对…

ARM-系统移植(开发环境搭建)

基于STM32MP157单片机 一、安装tftp服务器 首先保证ubuntu连接网络成功 1. 安装步骤 作用&#xff1a;完成ubuntu和开发板之间传输文件 1&#xff09;安装tftp服务器的安装包 sudo apt-get install tftpd-hpa tftp-hpa tftpd-hpa : 服务器端 tftp-hpa : 客户端 2&#x…

Docker Desktop 如何运行容器

第一次使用windows环境下的DockerDesktop记录下使用方法 1、配置镜像源&#xff0c;虽然配置了镜像源&#xff0c;但是在界面你还是搜索不到镜像&#xff0c;应该默认使用的是官方dockerhub的原因&#xff0c;后面可以手动创建避开这个问题。 2、运行系统的windows powershell…