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

news/2024/12/4 12:39: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 activeAuto-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 downcounterreaches zero, an update event is generated and counting restartsfrom 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 modeThis 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.ppmy.cn/news/151387.html

相关文章

11.PasswordEncoder详解与实战

security/day07 这节课我们开始讲PasswordEncoder&#xff0c;如果大家还有印象的话&#xff0c;我们前面有提到过PasswordEncoder: 为什么密码使用{noop}开头呢&#xff1f;我们也做出了相应的解释&#xff0c;这节课开始带大家真正的了解PasswordEncoder, PassworderEncoder…

全国十大名牌集成灶有哪些?选择哪个集成灶品牌比较好?

很多小伙伴都有过厨房装修的经历&#xff0c;其中购买厨电是比较费神的事情&#xff0c;毕竟市场上厨电品牌众多&#xff0c;型号也是各不同相同。那么如何才能购买到适合自己的厨电呢&#xff1f;这里有一份全国十大名牌集成灶名单&#xff0c;能登榜的集成灶品牌都是不错的品…

json库——jsoncpp

一、jsoncpp简介 JsonCpp是一个开源的C库&#xff0c;用于解析和生成JSON&#xff08;JavaScript Object Notation&#xff09;数据格式。JSON是一种轻量级的数据交换格式&#xff0c;广泛用于各种应用程序和网络服务中。 JsonCpp提供了简单和易用的API&#xff0c;可以方便地解…

thinkpad x1 carbon gen 6 无法充电(静电故障?)

突然发现无法通过usb-c 充电&#xff0c;或许后面还会导致电池亏电无法开机&#xff0c;只能通过bios放电临时解决&#xff1b;这次通过更新Intel ThuderBolt 固件解决&#xff0c;后续持续观察。 参考文章&#xff1a; 联想中国(Lenovo China)联想知识库 Lots of Lenovo lap…

Thinkpad 笔记本

命名规则&#xff1a; 以T460S为例&#xff1a; T代表系列&#xff0c;除T外&#xff0c;还有X,S,P,L,E和最近停产的W系列&#xff1b; 4代表尺寸&#xff0c;例如4代表14吋&#xff0c;2代表12吋&#xff0c;5代表15吋&#xff1b; 6代表发布年份&#xff0c;如5代表2015年…

android平板外接显示器,联想Yoga Book X曝光:即是安卓平板,还是外接显示器

联想Yoga Book X曝光&#xff1a;即是安卓平板&#xff0c;还是外接显示器 2020-07-16 01:00:09 9点赞 9收藏 20评论 最近&#xff0c;联想接二连三的新机被曝光&#xff0c;除了昨天看到的16:10屏、重不足1公斤的ThinkPad X1 nano&#xff0c;刚刚业内还曝光了一款有趣的产品&…

thinkpad10平板 linux,新款 ThinkPad 10 登场,考虑 Surface 3 朋友的另一选项

联想在 Computex 之前发布了一票笔记本产品(还有个 ChromeCast 挑战者)&#xff0c;其中还包括一个瞄准商业应用的 10 吋 Windows 平板 ThinkPad 10&#xff0c;「联想」到了微软最近推出的 Surface 3 吗&#xff1f;其实说它是联想所推出的类似产品也不为过&#xff0c;因为它…

Thinkpad分类

Thinkpad分为W、T、X、R、L、SL、E七个系列&#xff0c;后缀有i、p、t、s等&#xff0c; 其中&#xff1a; W——图形工作站 T——Thinkpad的代表机型&#xff0c;如果经济情况没问题&#xff0c;这是首选 X——便携机型 R——T系列的低价位版本&#xff0c;价格便宜&#xff…