标签归档:编程

代码重构

最近由于项目需要准备多现有项目进行二次重构,整理了一些关于重构的知识。

主要参考的是 Martin Fowler 的《重构:改善既有代码设计》和极客时间的王争在《设计模式之美》中关于重构的介绍。

一、重构的原则

什么是重构

  • 重构(名词):对软件内部结构的一种调整,目的是在不改变「软件之可察行为」前提下,提高其可理解性,降低修改成本。
  • 重构(动词):使用一系列重构准则/手法,在不改变「软件之可受观察行为」前提下,调整其结构。

重构是这样一个过程:它在一个目前可运行的程序上进行,企图在「不改变程序行为」的情况下(1)容易理解;(2)所有逻辑都只在唯一地点指定;(3)新的改动不会危及现有行为;(4)尽可能简单表达条件逻辑(conditional logic),使我们能够继续保持高速开发,从而增加程序的价值。

为什么重构

  • 改进软件设计
  • 使软件更易被理解
  • 助你找到bugs
  • 提高编程速度

何时重构

  • 三次法则〔The Rule of Three〕
    • Don Roberts 给了一条准则:
      • 第一次做某件事时只管去做;
      • 第二次做类似的事会产生反感,但无论如何还是做了;
      • 第三次再做类似的事,你就应该重构。
    • Tip: 事不过三,三则重构(Three strikes and you refactor)
  • 添加功能时一并重构
    • 帮助理解
    • 代码的设计无法帮助我轻松添加我所需要的特性
    • 新特性的添加就会更快速、更流畅。
  • 修补错误吋一并重构
  • 复审代码吋一并重构
  • 为什么重构有用
    • 原因
      • 难以阅读的程序,难以修改。
      • 逻辑复杂(duplicated logic)的程序,难以修改。
      • 添加新行为时需要修改既有代码者,难以修改。
      • 带复杂条件逻辑(complex conditional logic)的程序,难以修改。
    • 方法
      • 容易理解
      • 所有逻辑都只在唯一地点指定
      • 新的改动不会危及现有行为
      • 尽可能简单表达条件逻辑

如何发现代码质量问题

常规checklist

  • 目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?
  • 是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD等)?
  • 设计模式是否使用得当?是否有过度设计?
  • 代码是否容易扩展?如果要添加新功能,是否容易实现?
  • 代码是否可以复用?是否可以复用已有的项目代码或者类库?是否有重复造轮子?
  • 代码是否容易测试?单元测试是否全面覆盖各种正常和异常的情况?
  • 代码是否易读?是否符合编码规范(比如命名和注释是否使用得当、代码风格是否一致)?

业务需求checklist

  • 代码是否实现了预期的业务需求?
  • 逻辑是否正确?是否处理了各种异常情况?
  • 日志打印是否得当?是否方便debug排查问题?
  • 接口是否易用?是否支持幂等事务等
  • 代码是否存在并发问题?是否线程安全?
  • 性能是否有优化空间,比如,SQL、算法是否可以优化?
  • 是否有安全漏洞?比如,输入输出校验是否全面?

幂等:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

知会经理

不要告诉经理!

重构的难题

  • 数据库(Database)
  • 修改接口(Changing Interfaces)
  • 难以通过重构手法完成的设计改动
  • 何吋不该重构?——有时候既有代码实在太混乱,重构它还不如重新写一个来得简单

重构与设计

  • 「事先设计」(upfront design)可 以助我节省回头工的高昂成本。
  • 哪怕你完全了解系统,也请实际量测它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。

重构与性能(Performance)

  • 三种「编写快速软件」的方法
    • 「时间预算法」(time budgeting)——性能要求极高的实时系统
    • 「持续关切法」( constant attention)——设法保持系统的高性能
    • 「良好的分解方式」(well-factored manner)
      • 不对性能投以任何关切,直至进入性能优化阶段——那通常是在开发后期
      • 有比较充裕的时间进行性能调整(performance tuning)
      • 在进行性能分析时便有较细的粒度(granularity)

重构这个概念起源何处?

Ward Cunningham 和 Kent Beck 的 Smalltalk

二、单元测试

什么是单元测试?

单元测试是代码层面的测试,由研发自己来编写,用于测试“自己”编写的代码的逻辑的正 确性。单元测试顾名思义是测试一个“单元”,有别于集成测试,这个“单元”一般是类或 函数,而不是模块或者系统。

为什么要写单元测试?

写单元测试的过程本身就是代码 Code Review 和重构的过程,能有效地发现代码中的 bug 和代码设计上的问题。除此之外,单元测试还是对集成测试的有力补充,还能帮助我们快速 熟悉代码,是 TDD 可落地执行的改进方案。

如何编写单元测试?

写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻 译成代码。我们可以利用一些测试框架来简化单元测试的编写。除此之外,对于单元测试, 我们需要建立以下正确的认知:

单元测试为何难落地执行?

一方面,写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写;另一方面,国 内研发比较偏向“快、糙、猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾。最 后,关键问题还是团队没有建立对单元测试正确的认识,觉得可有可无,单靠督促很难执行 得很好。 编写单元测试尽管繁琐,但并不是太耗时; 我们可以稍微放低对单元测试代码质量的要求; 覆盖率作为衡量单元测试质量的唯一标准是不合理的; 单元测试不要依赖被测代码的具体实现逻辑; 单元测试框架无法测试,多半是因为代码的可测试性不好。

三、可测试性

什么是代码的可测试性?

粗略地讲,所谓代码的可测试性,就是针对代码编写单元测试的难易程度。对于一段代码, 如果很难为其编写单元测试,或者单元测试写起来很费劲,需要依靠单元测试框架中很高级 的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好。

编写可测试性代码的最有效手段

依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候, 可以通过 mock 的方法解依赖外部服务,这也是我们在编写单元测试的过程中最有技术挑 战的地方。

常见的 Anti-Patterns

常见的测试不友好的代码有下面这 5 种:

  • 代码中包含未决行为逻辑
  • 滥用可变全局变量
  • 滥用静态方法
  • 使用复杂的继承关系
  • 高度耦合的代码

四、大型重构——解耦

“解耦”为何如此重要?

过于复杂的代码往往在可读性、可维护性上都不友好。解耦保证代码松耦合、高内聚,是控 制代码复杂度的有效手段。代码高内聚、松耦合,也就是意味着,代码结构清晰、分层模块 化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。

代码是否需要“解耦”?

间接的衡量标准有很多,比如,看修改代码是否牵一发而动全身。直接的衡量标准是把模块 与模块、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重 构。

如何给代码“解耦”?

给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些其他的设计思想与原则,比 如:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则 等。当然,还有一些设计模式,比如观察者模式。

五、重构的建议

  • 关于命名 

    命名的关键是能准确达意。对于不同作用域的命名,我们可以适当地选择不同的长度。
    作用域小的变量(比如临时变量),可以适当地选择短一些的命名方式。
    除此之外,命名中也可以使用一些耳熟能详的缩写。
    我们可以借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命 名。 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。
    除此之外,命名要符合项目的统一规范,不要用些反直觉的命名。
    接口有两种命名方式:一种是在接口中带前缀“I”;另一种是在接口的实现类中带后缀“Impl”。
    对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,一种是不带前缀。这两种命名方式都可以,关键是要在项目中统一。

  • 关于注释

    注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。
    总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。
    注释本身有一定的维护成本,所以并非越多越好。
    类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。

  • 函数、类多大才合适?

    函数的代码行数不要超过一屏幕的大小,比如 50 行。
    类的大小限制比较难确定。

  • 一行代码多长最合适?

    最好不要超过 IDE 显示的宽度。当然,限制也不能太小,太小会导致很多稍微长点的语句被折成两行,也会影响到代码的整洁,不利于阅读。

  • 善用空行分割单元块

    对于比较长的函数,为了让逻辑更加清晰,可以使用空行来分割各个代码块。
    在类内部,成员变量与函数之间、静态成员变量与普通成员变量之间、函数之间,甚至成员变量之间,都可以通过添加空行的方式,让不同模块的代码之间的界限更加明确。

  • 四格缩进还是两格缩进?

    王争比较推荐使用两格缩进,这样可以节省空间,特别是在代码嵌套层次比较深的情况下。
    除此之外,值得强调的是,不管是用两格缩进还是四格缩进,一定不要用 tab 键缩进。

  • 大括号是否要另起一行?

    比较推荐将大括号放到跟上一条语句同一行的风格,这样可以节省代码行数。
    但是,将大括号另起一行,也有它的优势,那就是,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更加一目了然。

  • 类中成员的排列顺序

    在 Google Java 编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数。
    成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列。
    代码风格都没有对错和优劣之分,只要能在团队、项目中统一即可,不过,最好能跟业内推荐的风格、开源项目的代码风格相一致。

  • 关于编码技巧

    将复杂的逻辑提炼拆分成函数和类。
    通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多的情况。
    函数中不要使用参数来做代码执行逻辑的控制。
    函数设计要职责单一。

  • 统一编码规范

    项目、团队,甚至公司,一定要制定统一的编码规范,并且通过 Code Review 督促执行,这对提高代码质量有立竿见影的效果。
    移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数。
    用字面常量取代魔法数。

  • 建立一组可靠的测试环境。

    这些测试必须有自我检验(self-checking)能力。

  • 分解并重组长函数

    以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
    修改变量名
    搬移函数位置
    去除临时变量 Replace Temp with Query

  • 运用多态(Polymorphism)取代相关的条件逻辑

六、错误码

对于函数出错返回数据类型,王争总结了 4 种情况,它们分别是:错误码、NULL 值、空对 象、异常对象。

  1. 返回错误码
    C 语言没有异常这样的语法机制,返回错误码便是最常用的出错处理方式。而 Java、 Python 等比较新的编程语言中,大部分情况下,我们都用异常来处理函数出错的情况,极 少会用到错误码。
  2. 返回 NULL 值
    在多数编程语言中,我们用 NULL 来表示“不存在”这种语义。对于查找函数来说,数据 不存在并非一种异常情况,是一种正常行为,所以返回表示不存在语义的 NULL 值比返回 异常更加合理。
  3. 返回空对象
    返回 NULL 值有各种弊端,对此有一个比较经典的应对策略,那就是应用空对象设计模 式。当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替 代 NULL 值,来表示不存在的情况。这样,我们在使用函数的时候,就可以不用做 NULL 值判断。
  4. 抛出异常对象
    尽管前面讲了很多函数出错的返回数据类型,但是,最常用的函数出错处理方式是抛出异 常。异常有两种类型:受检异常和非受检异常。 对于应该用受检异常还是非受检异常,网上的争论有很多,但也并没有一个非常强有力的理 由,说明一个就一定比另一个更好。
    所以,我们只需要根据团队的开发习惯,在同一个项目 中,制定统一的异常处理规范即可。 对于函数抛出的异常,我们有三种处理方法:直接吞掉、直接往上抛出、包裹成新的异常抛出。

Python + IFTTT Webhooks + Crontab 定时抓取微博到 Day One

最近在使用 Day One app作为日记软件,想要自动同步自己的微博 post 到 Day One。本来可以使用 IFTTT 完成,但在使用 IFTTT 的时候发现现在 IFTTT 无法调用新浪微博的 API 了。因此我在自己的VPS 上搭建了一套类似流程,每天抓取自己新浪微博的新内容,利用 IFTTT 的 Webhooks 自动导入到 Day One 中。

相关工具

IFTTT

IFTTT,是一个新生的网络服务平台,通过其他不同平台的条件来决定是否执行下一条命令。即对网络服务通过其他网络服务作出反应。IFTTT得名为其口号“if this then that”。

就是如果 A 条件触发,那么 IFTTT 会自动执行 B 动作。

Day One

一款日记 APP,界面干净清爽,对照片的支持也很棒。

Crontab

Linux 平台的定时任务工具

抓取微博更新

很多 Python 的爬虫代码都需要进行微博登录。但我需要的功能不多,因此不需要调用微博API。只需要能够访问自己的账户,获取公开的 post 即可。

我找到了这个 repo 不需要进行微博登录即可爬取指定用户的 post。 https://github.com/dataabc/weibo-crawler

以此实现用 python 抓取自己每天新更新的微博到 VPS 的 csv 文件中。

CSV文件解析

对于获取的CSV文件中的信息,我只需要得到文本信息和图片信息。对于发送的多个图片,只获取第一张。

def parse_post_info(self, path):
    with open(path, 'r', encoding='UTF-8') as f:
        csv_reader = csv.reader(f, delimiter=',')
        line_count = 0
        for row in csv_reader:
            if line_count == 0:
                print("Header")
            else:
                self.text.append(row[2])
                self.image.append(row[4].split(",",1))
            line_count += 1
        self.num = line_count - 1

        print(self.text)
        print(self.image)
        print("Total posts: ", self.num)

生成 Webhooks

在 IFTTT 中,生成一个 Webhook 到 Day One 的 tablet。

我只需要获取微博文字,如果有图片的话获取第一张图片。具体的设置为:

发送 Webhooks 通知

在 IFTTT 中可以获取已经创建的 Event 的 Webhooks key

在 IFTTT 的service中找到 Webhooks (https://ifttt.com/maker_webhooks)

打开 Settings,在 https://maker.ifttt.com/use/ 后面的就是 Webhooks 的 key。

然后在 Python 代码中发送解析的所有微博 post 文本到 Webhooks。

def send_notice(self):
    url = f"<https://maker.ifttt.com/trigger/{self.eventname}/with/key/{self.key}>"
    for num in range(0, self.num):
        payload = {"value1": self.text[num], "value2": self.image[num]}
        headers = {"Content-Type": "application/json"}
        response = requests.request("POST", url, data=json.dumps(payload), headers=headers)
        print(payload)

这样运行这个 Python 脚本,选定日期的新微博就会被爬取、解析然后发送到 Webhooks 上。

IFTTT 会根据收到的 Webhooks 内容直接在设置好的 Day One 日记本中创建一条新的条目。

Crontab 定时更新

我在 VPS 上使用了 Crontab 每天定时检查微博有没有更新,更新了的话就抓取当天的微博发送到自己的日记本。关于 Crontab 的使用方法可以参考:https://man7.org/linux/man-pages/man5/crontab.5.html

比如希望在每天晚上8:30执行一个shell:

crontab -e

然后写入:

30 20 * * *  sh /home/usr/do_something.sh

这样就在VPS上利用 python 和 IFTTT 的Webhooks 实现了一个自动同步 weibo post 到 Day One app 的工具。

本来使用一个 IFTTT 就可以搞定的事情,但由于微博的 API 并不开放给 IFTTT,或者 IFTTT 没有进行相应的更新,导致只能手动去搭建一套这样的自动化流程。

可能有这样需要同步微博到 DayOne 需求的人不多吧(摊手)。

Git Commit – Angular Convention

使用 Git 的开发者会使用 git commit 进行代码提交,也会使用 -m 提交commit message。对于一些个人开发者,也许他们会觉得“这是我个人的项目,不用太在意git commit message 的格式或者规范”。

但是对于一个团队或者在开源项目上工作的话,对于 commit message 的质量就会有比较高的要求了。一个好的 Git commit message 是向项目的其他参与者进行提交信息说明的十分重要的途径,对于开发这个人而言也在未来回顾提交信息的时候十分有意义。

有时候我们在旧项目上使用 git log 的时候会发现一堆杂乱的 commit messages。很难读懂当时提交这条 commit 的原因,增加或者修改了哪些内容。所以,一个“好”的 commit message 的重要性在于:

  • 一些项目需要严谨的开发过程,以及清楚的可追溯性,相关的责任人。 当这些项目出现问题时,重要的是要知道确切的原因:哪些代码更改不正确,哪些用户会受此问题影响,为什么当时修改了代码等等。
  • 好的代码值得被“认真”地分享。开发者应该不希望自己辛苦写了一个礼拜的代码提交为“add some new features”。我觉得这是对于开发者自己劳动成果的一种不尊重,至少需要体现出解决了哪些问题,哪里比较“酷”。

所以为了其他人,也为了自己,请认真对待 commit messages 🙂

参考下面的 commit (来自网络):

e5f4b49 Re-adding ConfigurationPostProcessorTests after its brief removal in r814. @Ignore-ing the testCglibClassesAreLoadedJustInTimeForEnhancement() method as it turns out this was one of the culprits in the recent build breakage. The classloader hacking causes subtle downstream effects, breaking unrelated tests. The test method is still useful, but should only be run on a manual basis to ensure CGLIB is not prematurely classloaded, and should not be run as part of the automated build.
2db0f12 fixed two build-breaking issues: + reverted ClassMetadataReadingVisitor to revision 794 + eliminated ConfigurationPostProcessorTests until further investigation determines why it causes downstream tests to fail (such as the seemingly unrelated ClassPathXmlApplicationContextTests)
147709f Tweaks to package-info.java files
22b25e0 Consolidated Util and MutableAnnotationUtils classes into existing AsmUtils
7f96f57 polishing

每次提交的内容从长度到格式都不同,写法像是“意识流”(非褒义)。在 github 上很多项目都是类似的提交格式,但是也有比较清晰准确的 commit messages,比如 Linux kernel,读者可以参考。

开发者可以自己制定一套 commit 的格式,也可以使用流行的一些格式。这里想要分享的是我个人和团队在使用的一种 commit convention: Angularhttps://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines​github.com

概述

Angular 规定 commit message 的结构如下:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

header + body(可选) + footer(可选)

header

header 包含三部分:type + scope + subject。比如,

feat(lang): add polish language

(1)type

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动

在 type 后面增加“!”代表重大的代码更改。

(2)scope

用于说明 commit 影响的范围,比如数据层、控制层、或者项目名称等等。

(3)subject

commit 的简短描述,不超过50个字符。

  • 以动词开头,使用第一人称现在时,比如change,而不是changedchanges
  • 第一个字母小写
  • 结尾不加句号(.

body (非必须)

body 是详细描述的信息,例如:

More detailed explanatory text, if necessary.  Wrap it to 
about 72 characters or so. 

Further paragraphs come after blank lines.

- Bullet points are okay, too
- Use a hanging indent

footer (非必须)

(1)不兼容变动

如果当前代码与上一个版本不兼容,则 Footer 部分以BREAKING CHANGE开头,后面是对变动的描述、以及变动理由和迁移方法。

(2)关闭 Issue

如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue 。

Closes #234

示例

Commit message with description and breaking change footer

feat: allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for extending other config files

Commit message with ! to draw attention to breaking change

refactor!: drop support for Node 6

Commit message with both ! and BREAKING CHANGE footer

refactor!: drop support for Node 6

BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.

Commit message with no body

docs: correct spelling of CHANGELOG

Commit message with scope

feat(lang): add polish language

Commit message with multi-paragraph body and multiple footers

fix: correct minor typos in code

see the issue for details

on typos fixed.

Reviewed-by: Z
Refs #133

参考

https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines

https://www.conventionalcommits.org/en/v1.0.0/

https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html

Doxygen – 治好了我的代码注释强迫症

在编写大量代码时,或者与多人协作进行开发时,文字记录很重要(文档或注释),尤其对于开发者本人回顾之前写过的代码和 code reviewer 查看。目前,代码中的注释进是程序员们最经常使用的记录方法。但是,由于每个人的注释风格不同,有些开发者甚至对API并不进行任何注释描述,导致很多影响生产效率的问题,也增加了软件维护的成本。很多人对于怎样写注释也会困惑和反感。因此,使用一些代码文档工可以帮助这个开发流程高效、规范地进行。

使用这种工具我个人认为最大的帮助是可以节省时间,并提高生产力

  • 不用去构思如何去写注释,用哪一种风格去写,从而专注于构思代码。
  • 减少看其他人代码的时间成本。

Doxygen 就是这样一种编写软件参考文档的工具。该文档是直接以注释的形式写在代码中的,因此比较容易修改和查看。而且对于代码里的注释,Doxygen还可以为C++项目生成html,Latex和PDF的文档。

虽然使用过Doxygen的开发者可能并不会经常阅读由Doxygen生成的那些文档,而是更加倾向于阅读源代码里的注释。但是并不妨碍Doxygen成为一款很受欢迎而且高效的代码注释工具,至少我个人很喜欢Doxygen对于注释的种种规范。

关于如何使用Dxygen的适用方法,可以参照官方的教程,不会有比这个更加权威并且详细的说明了:https://www.doxygen.nl/manual/index.html

在之前分享过的C/C++ Coding Style(https://zhuanlan.zhihu.com/p/267645803)中,我介绍了使用Doxygen进行代码注释的一些风格,在这里详细介绍一些我经常使用的Doxygen属性。

一个例子

例如一个头文件中:

/*****************************************************************************
 * Portfolio Info
 ****************************************************************************/

/**
 * @file doxygen.h
 * @brief File containing example of doxygen usage for quick reference.
 *
 * Here typically goes a more extensive explanation of what the header
 * defines. Doxygens tags are words preceeded by either a backslash @\\
 * or by an at symbol @@.
 *
 * @author Chris Wu
 * @date 24 Oct 2019
 * @see <http://www.doxygen.nl/manual/index.html>
 */

/** @addtogroup DOXYGEN_API
 * @brief Doxygen api example.
 *
 * Detailed api description.
 *
 * @{
 */

#ifndef _TEMP_DOXYGEN_H
#define _TEMP_DOXYGEN_H

#ifdef __cplusplus
extern "C" {
#endif

/* INCLUDE FILES */
#include <stdint.h>
//#include <system_header2.h>

//#include "local_header1.h"
//#include "local_header2.h"

/* GLOBAL DEFINES */
/**
 * @brief Use brief, otherwise the index won't have a brief explanation.
 *
 * Detailed explanation.
 */
typedef enum eEnumMode
{
    ENUM_FIRST,  /**< Some documentation for first. */
    BOXENUM_SECOND, /**< Some documentation for second. */
    BOXENUM_ETC     /**< Etc. */
} tEnumMode;

/**
 * @brief Use brief, otherwise the index won't have a brief explanation.
 *
 * Detailed explanation.
 */
typedef struct BoxStruct
{
    int a;    /**< Some documentation for the member BoxStruct#a. */
    int b;    /**< Some documentation for the member BoxStruct#b. */
    double c; /**< Etc. */
} tBoxStruct;

/* GLOBAL VARIABLES */
extern int giValue;

/* GLOBAL FUNCTIONS */
/**
 * @brief Example showing how to document a function with Doxygen.
 *
 * Description of what the function does. This part may refer to the parameters
 * of the function, like @p param1 or @p param2. A word of code can also be
 * inserted like @c this which is equivalent to <tt>this</tt> and can be useful
 * to say that the function returns a @c void or an @c int. If you want to have
 * more than one word in typewriter font, then just use @<tt@>.
 * We can also include text verbatim,
 * when the language is not the one used in the current source file (but
 * <b>be careful</b> as this may be supported only by recent versions
 * of Doxygen). By the way, <b>this is how you write bold text</b> or,
 * if it is just one word, then you can just do @b this.
 *
 * @param [in] param1 Description of the first parameter of the function.
 * @param [out] param2 The second one, which follows @p param1, and represents output.
 *
 * @return Describe what the function returns.
 * @retval XXX_OK if successful.
 *
 * @see doxygen_theSecondFunction
 * @see Box_The_Last_One
 * @see <http://website/>
 * @note Something to note.
 * @warning Warning.
 */
int doxygen_theFirstFunction(int param1, int param2);

/**
 * @brief A simple stub function to show how links do work.
 *
 * Links are generated automatically for webpages (like <http://www.google.com>)
 * and for structures, like sBoxStruct. For typedef-ed types use
 * #tBoxStruct.
 * For functions, automatic links are generated when the parenthesis () follow
 * the name of the function, like doxygen_theFirstFunction().
 * Alternatively, you can use #doxygen_theFirstFunction.
 * @return @c NULL is always returned.
 */
void doxygen_theSecondFunction(void);

#ifdef __cplusplus
}
#endif

#endif /* _TEMP_DOXYGEN_H */
/** @}*/

版权声明注释

/*****************************************************************************
 * Portfolio Info
 ****************************************************************************/

我会把项目的版权声明信息放在这。

文件描述注释

/**
 * @file header.h
 * @brief Brief file introduction.
 *
 * Detailed file introduction.
 *
 * @author Name
 * @date day month year
 * @see Related link.
 */

在这里列出这个文件的名字,和一些基本信息:

  • @file: 文件名
  • @brief:文件一句话介绍
  • @author:文件作者
  • @date:修改日期
  • @see:额外的一些参考信息,比如有用过的链接

API Group

/** @addtogroup DOXYGEN_API
 * @brief Doxygen api example.
 *
 * Detailed api description.
 *
 * @{
 */
  • API codes…
/** @}*/

这两段代码搭配使用,一前一后,中间包含API代码。这部分定义了这个文件的API group,可以在Doxygen生成的文件中查看到其中包含的所有API group(比如变量,类,函数)信息。

变量前的注释

/**
 * @brief Use brief, otherwise the index won't have a brief explanation.
 *
 * Detailed explanation.
 */
typedef struct BoxStruct
{
    int a;    /**< Some documentation for the member BoxStruct#a. */
    int b;    /**< Some documentation for the member BoxStruct#b. */
    double c; /**< Etc. */
} tBoxStruct;

对于一些需要说明的变量,可以在变量前加上一段Doxygen注释,方便度代码的人查看,也可以声称在比如HTML等文档中。

API 函数注释

/* GLOBAL FUNCTIONS */
/**
 * @brief Example showing how to document a function with Doxygen.
 *
 * Description of what the function does. This part may refer to the parameters
 * of the function, like @p param1 or @p param2. A word of code can also be
 * inserted like @c this which is equivalent to <tt>this</tt> and can be useful
 * to say that the function returns a @c void or an @c int. If you want to have
 * more than one word in typewriter font, then just use @<tt@>.
 * We can also include text verbatim,
 * when the language is not the one used in the current source file (but
 * <b>be careful</b> as this may be supported only by recent versions
 * of Doxygen). By the way, <b>this is how you write bold text</b> or,
 * if it is just one word, then you can just do @b this.
 *
 * @param [in] param1 Description of the first parameter of the function.
 * @param [out] param2 The second one, which follows @p param1, and represents output.
 *
 * @return Describe what the function returns.
 * @retval XXX_OK if successful.
 *
 * @see doxygen_theSecondFunction
 * @see Box_The_Last_One
 * @see <http://website/>
 * @note Something to note.
 * @warning Warning.
 */
int doxygen_theFirstFunction(int param1, int param2);

对于所有头文件的函数,都需要进行函数注释。

  • @param:标记变量 [in] [out]表示输入输出方向
  • @return:返回值描述
  • @retval:具体返回值及其含义
  • @see:link信息
  • @note:备注信息
  • @warning:需要函数使用者注意的信息,比如:功能未经完全验证

总结

我对自己的要求是,对于 public 头文件中的所有信息,都应该进行详细的注释,方便使用者查看。而在代码源文件中可以不详细注释,自己可以理解。但是也需要按照Doxygen的格式列出基本信息方便回顾代码的时候提醒自己,让自己了解自己当时在想些什么,为什么要这么做。

对于一个开发团队,尤其是大公司,都会有严格规定的标准,包括代码风格,各种git hook,还有类似Doxygen这种文档工具。有些工程师会觉得这些“标准”很繁琐,很难要求自己遵守。但是不可否认的是,各种代码标准的目的不是以折磨程序员为乐趣,而是为了提高程序员的生产力,提高代码的可读性,可维护性,可靠性。

但是有时候,为了追求标准化而去不断强调“标准”这个概念,甚至花费大量的时间精力去专注在“标准”们上也是很荒唐的。所以无论我们使用哪些“标准”,参考各大开发团队的标准,形成一套适合自己团队的开发规范即可,内容永远大于形式。

这不是一篇Doxygen的使用教学,而是想推荐类似Doxygen这种代码文档注释管理工具,对于如何使用Doxygen,强烈建议参考官方教程:

Doxygen Manual: Overview​www.doxygen.nl

因为Doxygen对我和我们团队的开发提供了很大的帮助,因此推荐,祝开发愉快:D

浅谈HAL设计(3)- 回调函数

在设计HAL,或者把代码结构分层的时候我们不可避免会遇到这种情况,一般函数调用顺序总是高级别函数调用低级别函数,比如HAL调用dirver层,或者app调用middleware。但是总有一些情况需要我们在较低级别的函数中调用较高级别的函数,这时候回调函数(callback)就十分有用了。

什么是回调

举一个例子,老板让张三去参加一个培训,培训时间要1000秒:

void start_training(char* name)
{
    delay(1000);
}

老板还想要让张三去给自己买一杯咖啡,时间需要100秒

void buy_coffee(char* name)
{
    delay(100)
}

老板同时执行了两个并行的任务线程:

execute_task_1()
{
    start_training("ZhangSan")
}
execute_task_2()
{
    buy_cooffee("ZhangSan")
}

张三不能去买咖啡,因为他还在参加培训。于是,老板只能一遍一遍发这个命令,或者等到张三结束培训之后再告诉张三。老板很恼火,觉得自己浪费了很多时间。

张三建议老板使用“回调”:

typedef void(*boss_cb)(char* name);
void start_training(char* name, boss_cb callback)
{
    delay(1000);
    callback(name);
}
void buy_coffee(char* name)
{
    delay(100)
}

于是老板只需要一条指令:

execute_task_1()
{
    start_training("ZhangSan", buy_coffee)
}

张三在培训完会直接去执行买咖啡的指令,老板不用一直等着或者反复催促,节省了老板很多时间,老板很满意,给张三加了薪。

张三在培训时候使用的就是回调函数,他不需要知道回调函数的具体内容,只提供这个接口。具体的内容由老板实现,老板让干什么就干什么,可以培训完去买咖啡,可以培训完去开车,也可以培训完去参加会议。

所以,回调函数是对某段代码的引用,该代码作为参数传递给其他代码,该代码允许较低级别的软件层调用较高级别的层中定义的函数[1]。在软件设计中,回调这种机制允许dirver这种较低层代码中设计某种接口,然后将具体如何实现这个接口功能留给上层的应用程序层。C语言中可以通过传递函数指针实现。

最简单的回调函数只是作为参数传递给另一个函数的函数指针。 在大多数情况下,回调包含三部分:

  • 回调函数
  • 注册回调函数
  • 执行回调函数

下图显示了这三个部分在回调实现中是如何工作的[2]:

一个简单的例子

在这个例子中有三个文件:

  • callback.c
  • reg_callback.h
  • reg_callback.c
/* callback.c */
#include<stdio.h>
#include"reg_callback.h"
/* callback function definition goes here */
void my_callback(void)
{
    printf("inside my_callback\n");
}
int main(void)
{
    /* initialize function pointer to
    my_callback */
    callback ptr_my_callback=my_callback;                           
    printf("This is a program demonstrating function callback\n");
    /* register our callback function */
    register_callback(ptr_my_callback);                             
    printf("back inside main program\n");
    return 0;
}
/* reg_callback.h */
typedef void (*callback)(void);
void register_callback(callback ptr_reg_callback);
/* reg_callback.c */
#include<stdio.h>
#include"reg_callback.h"
/* registration goes here */
void register_callback(callback ptr_reg_callback)
{
    printf("inside register_callback\n");
    /* calling our callback function my_callback */
    (*ptr_reg_callback)();                                  
}

编译运行后可以得到输出:

This is a program demonstrating function callback
inside register_callback
inside my_callback
back inside main program

正如图片里所展示的关系,较高层的函数将较低层的函数作为普通调用来调用,并且回调机制允许较低层的函数通过指向回调函数的指针来调用较高层的函数。使用回调函数,相当于把两个异步的任务关联了起来。

所以,在C语言中回调函数只不过是将函数指针传递到需要调用回调函数的代码,使用这种方法,我们可以实现一些比如,错误处理,退出前清理内存等很有实用性的功能。

Reference

[1] https://en.wikipedia.org/wiki/Callback_%28computer_programming%29

[2] https://www.beningo.com/embedded-basics-callback-functions/

My C/C++ Coding Style

编码风格对于软件开发者而言十分重要,对大型的开发团队更是如此,每个公司也都有自己的风格规定。在这里分享一套我在 C/C++项目中使用的 coding style。这套编码风格参考整理了 Google C++ coding style,RDK coding guidelines,NASA coding style,和一些开源的编码风格(见附录)。

规范编码风格的目的不是形式化,而是为了提高写代码效率和代码的一致性,同事提高代码的可读性与可维护性。比如注释部分我花了很大的篇幅介绍Doxygen工具的注释风格,并且对每一个public API都要求有清晰、详细的注释。

将整理好的风格整理成Astyle的配置,写一个batch脚本或者在git hook中添加Astyle,即可使用batch文件或者git commit后自动修改代码格式。

1 Project Structure

1.1 File Guards

All header files should use #define to avoid multi-declaration. Naming format should be <PLATFORM>_<FILE>_H_For example, the file adsp/dirver/include/adsp_driver_adc.h in platform adsp should have the following guard.:

#ifndef ADSP_DRIVER_ADC_H_
#define ADSP_DRIVER_ADC_H_

...

#endif  /* ADSP_DRIVER_ADC_H_ */

1.1 Order of Includes

Include headers in the following order: Related header, C system headers, C++ standard library headers, other libraries’ headers, your project’s headers.For example, In adsp/dirver/src/adsp_driver_adc.c, whose main purpose is to implement or test the stuff in adsp/dirver/include/adsp_driver_adc.h, order your includes as follows:

  1. self.h.
  2. A blank line
  3. C/C++ system headers (more precisely: headers in angle brackets with the .h extension), e.g. <unistd.h>, <stdlib.h>.
  4. A blank line
  5. Other libraries’ .h files.
  6. Your project’s .h files.

1.2 File Names

File names should include both platform name and module functions. For example:

  • A driver file name example is mcu_driver_adc.c.
  • A utility file name example is utility_crc32.c.
  • A HAL file name example is hal_adc.c, hal_adc.h.
  • A app file name example is app_gateway.c.
  • A board file name example of project XXX isxxx_audio.c.

2. Code Format

2.1 Brace Placement

Allman style is used for brace placement, e.g.

while (x == y) 
{
    something(); 
    somethingelse(); 
}

2.2 Indentations

Use 4 spaces rather than tabs as printers as users might have different tab settings.Use single line spacing between logical blocks of code.Use double line spacing between functions.Linux(LF) end line style is used.

2.3 Code Length

Code in single line should not exceed 80 characters. When breaking lines, use the natural logical breaks to determine where the newline goes. Indent the continuation line to illustrate its logical relationship to the rest of the code in the line.

if (thisVariable1 == thatVariable1 || thisVariable2 == thatVariable2 || thisVariable3 == thatVariable3)
    bar();

becomes:

if (thisVariable1 == thatVariable1 ||
        thisVariable2 == thatVariable2 ||
        thisVariable3 == thatVariable3)
    bar();

2.4 Brackets

Use a pure-block, fully bracketed style for blocks of code. This means put brackets around all conditional code blocks, even one-line blocks.

if(statement == true)
{
    foo_true();
}
else
{
    foo_false();
}

2.5 Spaces

  • Insert space padding around operators. E.g.,
if (foo == 2)
    a = bar((b - c) * a, d--);

3. Naming Conventions

  • All identifiers (variables, constants, Classes etc. ) declared should have meaningful names.
  • Have naming conventions to differentiate between local and global data.
  • Identifiers may have their types attached to their names for clarity and consistency.
  • In case where the language has support for header file, ensure all user defined header file should have the same name as the source file that is referenced in.
  • Names should be readable and self documenting. Abbreviations and contractions are to be discouraged. Abbreviations are allowed when they follow common usage within the domain.
  • Identifiers should not exceed 31 characters.

3.1 Files

File names should be named with lower case and underscores.

adsp_driver_adc.c 
adsp_driver_adc.h

3.2 Macros

Macro names should be named with all capitals and underscores.

#define ROUND(x) ...
#define PI_ROUNDED 3.0

3.3 Variables

Data TypePrefix/postfixSample Variable Names
general variablelowerCamelCasesequenceNo
constkconst int32_t kSequenceNo
staticsstatic int32_t sSequenceNo
enumerated data typeeeCapabilityMode
arrayaint32_t aSequenceNo[10]
pointerpint32_t *pSequenceNo
globalggAudioStruct
structPascalCase_sAudioStruct_s
typedefPascalCase_ttypedef struct AudioInfo_st { } AudioInfo_t;
member variable (C++)mmClassMember
class (C++)Pascal Caseclass AudioObject
template (C++)Ttemplate<…> class TAudioTemp
namespace (C++)Nnamespace NAudioName …
  • Use <stdint.h> (uint8_t, int32_t etc).
  • Use the smallest required scope.
  • Variables in a file (outside functions) are always static.
  • Avoid to use global variables (use functions to set/get static variables).
  • # &,* is aligned to names, e.g., uint32_t *pAddress

3.4 Functions

Global function names start with lower case module name + underscore + camel case function name. E.g., one function in doxygen.c is

uint32_t doxygen_theFirstFunction(uint32_t param1, uint32_t param2);

one function in adsp_driver_adc.c is

void adsp_driver_adc_readValue(uint8_t channel);

Local static function names apply lowerCamelCase rule, e.g., one function in adsp_driver_adc.c is

// file adsp_driver_adc.c
static void getInstance(void)

Functions in C++ class should always apply PascalCase rule, e.g. ,

class AudioObject
{
    uint8_t *GetObjectName(void);
}

4. Comments

Comments are used for the benefits of code readers as well as developers. So it is recommended that all comments should be written with simple and straightforward words, and English words only.

Doxygen is applied to generate documents according to code comments. Therefore, the rules of Doxygen should be strictly followed.

For better document graphic illustration, Grapghviz is recommended to be installed and used in Doxygen. Both Doxygen and Grapghviz can be installed in Cygwin.

Download link:

Grapghviz

https://graphviz.gitlab.io/_pages/Download/windows/graphviz-2.38.msi​graphviz.gitlab.io

Doxygen

http://doxygen.nl/files/doxygen-1.8.18-setup.exe​doxygen.nl

4.1 File Banners

Every header file should have a function banner as follows :

/*****************************************************************************
 * Portfolio Info
 ****************************************************************************/
 
/**
 * @file header.h
 * @brief Brief file introduction.
 *
 * Detailed file introduction.
 *
 * @author Name
 * @date day month year
 * @see Related link.
 */

/** 
 * @addtogroup API name
 * @brief Brief API description.
 * 
 * Detailed api description.
 * 
 * @{
 */

#ifndef _HEADER_NAME_H
#define _HEADER_NAME_H

#ifdef __cplusplus
extern "C" {
#endif

/* INCLUDE FILES */

/* GLOBAL DEFINES */

/* GLOBAL VARIABLES */

/* GLOBAL FUNCTIONS */

 ...

#ifdef __cplusplus
}
#endif

#endif /* _HEADER_NAME_H */
/** @}*/

Every source file should have a function banner as follows :

/*****************************************************************************
 * Portfolio Info
 ****************************************************************************/
 
/**
 * @file file.c
 * @brief Brief introduction.
 *
 * Here typically goes a more extensive explanation of what the source contains.
 *
 * @author Name
 * @date day month year
 * @see Related link.
 */

/* INCLUDE FILES */

/* MODULE CONSTANTS */

/* MODULE VARIABLES */

/* MODULE PROTOTYPES */

/* MODULE FUNCTIONS */

4.2 Function Banners

Detailed function banners should be used in header files since the corresponding c file(s) are not necessarily made available to the user, nor should the user need to read the c file in order to understand how the functions there should be used. Everything should be made obvious from the header file alone.

For static functions in c file(s), function banners are required to use for developing maintenance.

Every function should have a function banner as follows:

/**
 * @brief Brief introduction.
 *
 * Detailed introduction.
 *
 * @param [in] param1 Input parameter.
 * @param [out] param2 Output parameter.
 *
 * @return Describe what the function returns.
 * @retval XXX_OK return value.
 * 
 * @see Related link.
 * @note Something to note.
 * @warning Warning.
 */

4.3 Comments in Codes

For comments in codes, /* */ is suggested to use. And space are recommended to add before and after statements. E.g., Use /* example comment */ instead of /*examplecomment*/

5. Code Format Tool

Windows

AStyle is used to unify the code format. Before running the batch file, make sure AStyle.exe is included in system PATH. The format rules are configured in file config.astyle and all codes will be formated by running astyle.bat.

Reference

Google C++ coding style (good)
https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/scoping/

RDK coding guidelines
https://wiki.rdkcentral.com/display/RDK/Coding+Guidelines

NASA coding style (bad)
http://web.archive.org/web/20190125125043if_/http://homepages.inf.ed.ac.uk/dts/pm/Papers/nasa-c-style.pdf

C/C++ coding style by Fred Richards (good)
http://index-of.co.uk/C++/C%20&%20C++%20Programming%20Style%20Guidlines.pdf

Indian Hill C style
https://www.maultech.com/chrislott/resources/cstyle/indhill-cstyle.pdf

浅谈HAL设计(2)- 设计模式与实例

为什么使用设计模式

在软件工程中,设计模式(design pattern)是解决软件设计中常见问题的通用可重复解决方案。 设计模式不是可以直接转换为代码的最终设计。 它是关于如何解决可以在许多不同情况下使用的问题的描述或模板。

设计模式可以通过提供经过测试的,经过验证的开发范例来加快开发过程。 有效的软件设计需要考虑直到实施的后期才变得可见的问题。 重用设计模式有助于防止可能引起重大问题的细微问题,并提高熟悉模式的开发人员和架构师的代码可读性。

通常,人们只了解如何将某些软件设计技术应用于某些特定的问题。 这些技术很难应用于更广泛的问题。 而设计模式提供了一种更通用的解决方案,并且易于移植和使用。

此外,使用设计模式允许开发人员使用众所周知的,易于理解的名称进行交流,减小了沟通成本。常见的设计模式也可以随着时间的流逝而得到改进,使其比开发人员的临时设计更为可靠。

工程实例

设计思路

设计目的是要实现一个通用的 HAL,适用于各种不同的单板(board)项目,以及不同的芯片(platform)。各种 boards 和 platforms 具有类似的属性和功能。因此这里参考了抽象工厂设计模式,设计如下的包含 HAL 的工程。整个工程的文件结构的组成:

├─ include
│    └─ hal
├─ main
├─ app
├─ hal
├─ board
│    └─ hal
└─ platform
      ├─ hal
      ├─ cpu
      └─ driver

工程主要由四部分组成:

  • app:整个工程的上层 application 应用软件;
  • hal:上下层连接的硬件抽象层 HAL;
  • board:针对不同的硬件单板的定制化配置;
  • platform:针对不同的平台芯片的底层代码,对于所有的 board 都适用。例如,可以包含STM系列,TI的TMS,ADI的SHARC,NXP的S32等等不同的MCU、DSP芯片的驱动层代码。

其中,app 只需要调用 HAL 的 API 即可实现对硬件的使用。board 包含对单板的各种位置和初始化代码。platform 包含底层基础代码,操作硬件寄存器,适用于各种 board 单板。

HAL 相关的代码由于使用了设计模式,所以使用 C++实现,其他代码均使用 C 实现。

使用这种设计思路,使 HAL 这部分代码不需要被频繁修改,用户在使用的时候,只需要按照HAL所规定的API,添加对应 platform 类型(比如C2000)和 board 类型(比如 demodemo)的实现代码即可。这样,不同层之间的代码耦合度可以大大降低,可读性、可维护性也会有很大提高。

这种设计层次清晰,调用简单,但是不足之处就是对于内存有一些要求。因为使用了很多动态成员变量,和一些C++特性,对于RAM有一些要求,所以对于一些RAM空间有限的小型MCU/SOC并不适用。

对于内存有限的MCU,我设计了一套相似的 HAL lite 版本,全部由C语言实现,并且使用静态成员注册modules,避免程序半途因为RAM不够而崩溃。由于整体思路类似,就不再赘述。

HAL 的初始化与构建

我们假设一个简单的使用情景:

  • 一块名字叫做 demodemo 的单板;
  • 使用 TI 的 C2000 芯片;
  • 需要使用这个芯片的 ADC 的功能。

main函数中通过调用 hal_platform_init(); 实现对所对应 platform(也就是demodemo + C2000)的初始化,文件位置 /board/hal/demodemo_c2000_platform.cpp。部分代码:

bool DemodemoPlatform::Init()
{
    bool error = false;

    error = bsp_init();

    for (std::list<Driver *>::iterator it = mpDriverList.begin();
            it != mpDriverList.end();
            ++it)
    {
        error = (*it)->Init();
    }

    return error;
}

uint32_t hal_platform_init(void)
{
    ErrorCode_t status = NO_ERROR;
    Platform::PlatformBuilder *pPlatformBuilder = nullptr;

    /*construct the platform*/
    pPlatformBuilder = new DemodemoMcuPlatform::DemodemoMcuPlatformBuilder();
    if (nullptr != pPlatformBuilder)
    {
        gpPlatform = pPlatformBuilder->Construct();
        /* Initialize the specific platform*/
        gpPlatform->Init();
    }
    else
    {
        status = ERROR_NO_MEMORY;
    }

    return status;
}

这里使用了抽象工厂模式,可以针对不同的 platform 生成相应的实例。其中,首先通过:

Platform *DemodemoMcuPlatform::DemodemoMcuPlatformBuilder::Construct()
{
    DemodemoMcuPlatform *pPlatform = nullptr;
    pPlatform = new DemodemoMcuPlatform(this);
    HardwareFactory *pHardwarefactory = nullptr;
    pHardwarefactory = new DemodemoMcuHardwareFactory();
    Soc *pSoc = pHardwarefactory->CreateSoc();
    pPlatform->AddHardware(pSoc, pPlatform);

#if HAVE_ADC
    Adc *pAdc = pHardwarefactory->CreateAdc();
    pPlatform->AddHardware(pAdc, pPlatform);
#endif /* HAVE_ADC */

    delete pHardwarefactory;

    return pPlatform;
}

生成 demodemo 工厂实例,并通过CreateXxx(),AddHardware(),注册所需要使用的外设。

然后使用 gpPlatform->Init() 初始化 platform:

  1. 单板初始化:调用bsp_init() 做一些 demodemo单板一些必要的初始化,比如电源、时钟、中断、全局变量等的初始化。
  2. driver 初始化:通过
for (std::list<Driver *>::iterator it = mpDriverList.begin();
        it != mpDriverList.end();
        ++it)
{
    error = (*it)->Init();
}

实现对这个 platform 注册的所有外设的初始化,比如timer,UART,CAN,I2C等,这个例子只使用了一个ADC作参考。

这样,所有的与 HAL 有关的外设都创建了一个适用于 demodemo 单板的实例,当我们需要调相应的 HAL 的时候,可以在对应的 hal_xxx.cpp 中通过:

static inline Adc* getAdcInstance()
{
    static Adc* spAdc = nullptr;

    if (nullptr == spAdc)
    {
        Platform* pPlatform = static_cast<Platform*>(hal_platform_getInstance());
        Driver* pDriver = const_cast<Driver*>(pPlatform->GetHardware("Adc"));
        spAdc = static_cast<Adc*>(pDriver);
    }

    return spAdc;
}

得到所注册的外设(比如ADC)。然后再对外设模块具体操作即可:

uint32_t hal_adc_init(void)
{
    Adc* pAdc = getAdcInstance();

    if (nullptr != pAdc)
    {
        pAdc->Init();
        return NO_ERROR;
    }
    else
    {
        return ERROR_NO_MEMORY;
    }
}

Driver 如何被 App 调用

根据设计,app 在使用的过程中不需要知道底层驱动如何实现,而是可以调用 HAL API 实现对相应 driver 的使用。例如,可以这样使用 hal_adc_read() 读取 ADC 的数值:

/**
 * app.c
 */
void app_run(void)
{
    /* read ADC channel 1 value */
    printf("APP: Task triggered \n");
    hal_adc_read(1); // read ADC channel 1 value
}

这个 API 在文件 hal_adc.h 中 (位置 /include/hal),这个API是面向所有HAL的使用者,因此放在 /include/ 这个文件夹中。

对应文件 hal_adc.cpp 实现了具体操作,文件位置 /hal/src/hal_adc.cpp,并不允许 HAL API 使用者修改。

/**
 * hal_adc.cpp
 */
#include "../../include/hal/hal_adc.h"

#include <stdio.h>

#include "../../include/hal/hal_platform.h"
#include "../../hal/include/platform_interface.h"

#if HAVE_ADC

static inline Adc* getAdcInstance()
{
    static Adc* spAdc = nullptr;

    if (nullptr == spAdc)
    {
        Platform* pPlatform = static_cast<Platform*>(hal_platform_getInstance());
        Driver* pDriver = const_cast<Driver*>(pPlatform->GetHardware("Adc"));
        spAdc = static_cast<Adc*>(pDriver);
    }

    return spAdc;
}

uint32_t hal_adc_init(void)
{
    Adc* pAdc = getAdcInstance();

    if (nullptr != pAdc)
    {
        pAdc->Init();
        return NO_ERROR;
    }
    else
    {
        return ERROR_NO_MEMORY;
    }
}

uint16_t hal_adc_read(uint8_t Channel)
{
    Adc* pAdc = getAdcInstance();

    if (NULL != pAdc)
    {
        printf("HAL: ADC read triggered \n");

        return pAdc->Read(Channel);
    }

    return 0;
}

#endif /* HAVE_ADC */

HAL 通过调用 driver_interface 中定义的不同 CPU 的 ADC read 函数,实现对底层 driver 的调用,位置 /hal/include/driver_interface.h。

/**
 * driver_interface.h
 */

#ifndef HAL_INCLUDE_DRIVER_INTERFACE_H_
#define HAL_INCLUDE_DRIVER_INTERFACE_H_

/* INCLUDE FILES */
#include <string>
#include <stdint.h>

#include "../../board/include/board.h"

#if HAVE_ADC
//#include "../../include/hal/hal_adc.h"
#endif

/* GLOBAL FUNCTIONS */
/**
 * @brief Base class for all HAL drivers.
 *
 */
class Driver
{
public:
    virtual bool Init() = 0;

    virtual ~Driver() { };

    virtual const std::string &GetName() const
    {
        return mName;
    };

protected:
    Driver(): mName("Driver") {};
    std::string mName;
};

/**
 * @brief SoC chip type abstract class, derived from Driver.
 *
 */
class Soc : public Driver
{
public:
    virtual bool Init() = 0;

    virtual ~Soc() { };

protected:
    Soc() { };
};

#if HAVE_ADC
/**
 * @brief ADC driver abstract class, derived from Driver.
 *
 */
class Adc : public Driver
{
public:
    virtual bool Init() = 0;

    virtual uint16_t Read(uint8_t channel) = 0;

    virtual ~Adc() { };

protected:
    Adc() { };
};
#endif /* HAVE_ADC */

#endif /* HAL_INCLUDE_DRIVER_INTERFACE_H_ */

对上述HAL的driver 接口,可以用以下方式实现。根据MCU种类不同,用户可以定义自己的实现内容。例如,对于C2000 ADC 的 source 代码,可以这样实现,位置 /platform/hal/c2000_drivers.cpp:

/**
 * c2000_drivers.cpp
 */

 /* INCLUDE FILES */
#include "c2000_drivers.h"

#if HAVE_ADC
#include "../../platform/driver/c2000_driver_adc.h"
#endif

/* MODULE FUNCTIONS */
/**
 * S32K SOC class implementation
 */
C2000Soc::C2000Soc()
{
    mName.assign("Soc");
}

bool C2000Soc::Init()
{
    bool error = false;

    //error = s32k_driver_soc_init();

    return error;
}

C2000Soc::~C2000Soc()
{
}

#if HAVE_ADC
/**
 * S32K ADC class implementation
 */
C2000Adc::C2000Adc()
{
    mName.assign("Adc");
}

bool C2000Adc::Init()
{
    c2000_driver_adc_init();
    return true;
}

uint16_t C2000Adc::Read(uint8_t channel)
{
    return c2000_driver_adc_read(channel);
}

C2000Adc::~C2000Adc()
{
}
#endif /* HAVE_ADC */

这样,一个衔接上层的 app 与 底层的 ADC driver 的 HAL 就实现了。实例中只给出了 ADC 的实现,实际应用中可以根据需要添加 timer,UART,DAI等外设和服务。

执行 main.c 主函数,读取三次C2000 ADC 的数值:

int main(int argc, char **argv)
{
    printf("HAL DEMO STARTS...\n");
    printf("\n***Stage 1: INIT All***\n");
    hal_platform_init();
    app_init();

    printf("\n***Stage 2: START All***\n");
    hal_platform_start();
    app_start();

    printf("\n***Stage 3: RUN Application***\n");
    while (Global.system_running)
    {
        app_run();

        if (++Global.time > 2) break;
    }

    return 0;
}

我们可以得到结果:

总结

这个 HAL 的设计思路就是通过把整个工程的代码分为 app, board, hal, platform 这4层,实现 app->hal->driver 的调用,并且易于移植和调用。

优点:

  • 更好的可读性:更多抽象,更清楚的逻辑;
  • 更好的可维护性:每部分的代码可以单独维护,出现问题根据错误码方便查找追溯;
  • 更好的可移植性:对于不同的单板,只需要修改 board 层代码有针对地修改即可,其他层代码可以完全共用;
  • 更标准化:减少不同工程师的冲突。

缺点:

  • 文件数量多,对内存的动态操作较多,会降低一定的稳定性;
  • 代码量更大,需要的flash RAM空间会增多;
  • 执行速度更慢:因为要调用的函数嵌套更多,入栈出栈的次数会更多;
  • 对公共代码的修改要谨慎,保证其可靠性,避免影响其他 platform。

to be continued…

参考

https://en.wikipedia.org/wiki/Software_design_pattern

Design Patterns and Refactoring

浅谈HAL设计(1)- 介绍

When you have seen one ant, one bird, one tree, you have not seen them all. – E. O. Wilson

前一段时间由于工作需要,参与设计了HAL代码,实现 application 和 hardware driver 的分离,在此期间学习并且总结了一些HAL的相关知识,在这里简单梳理与延伸。

这个HAL系列的文章主要想通过以下几个方面讨论与HAL有关的一些知识:

  • HAL的介绍
  • 如何设计好HAL
  • 如何利用设计模式设计HAL
  • 回调函数的使用

HAL(Hardware Abstraction Layer)硬件抽象层

许多早期的计算机系统没有任何形式的硬件抽象。这意味着为该系统编写程序的任何人都必须知道每个硬件设备如何与系统的其余部分进行通信。这对软件开发人员来说是一个巨大的挑战,因为他们必须知道系统中每个硬件设备如何工作才能确保软件的兼容性。使用硬件抽象,而不是直接与硬件设备通信的程序,它将程序传达给操作系统该设备应执行的操作,然后,操作系统会向该设备生成硬件相关的指令。这意味着程序员不需要知道特定设备的工作方式,就能使他们的程序与设备兼容。

由于部分硬件厂商不想把自己的核心代码公开,如果把代码放在内核空间里就需要遵循GUN License,会损害厂家的利益。所以,Google为了响应厂家在Android的架构里提出HAL的概念,把对硬件的支持分为用户空间和内核空间,而HAL层就属于这里面的用户空间,该部分代码遵循Apache License,所以厂家可以把核心的代码实现在HAL层,无需对外开放源代码[3]。

一个好的比喻是交通运输的抽象。骑自行车和开车都是一种交通方式,它们都有共同点,比如需要使用脚、都有轮子。因此人们可以指定抽象的“drive”,然后让实现者决定是骑自行车还是开车。抽象了“轮式地面运输”功能,并封装了“如何驾驶”等详细信息。

所以抽象和封装是软件设计中很重要的概念,在计算机中,芯片中都可以抽象出很多模块。硬件抽象层(HAL)位于软件堆栈中的应用程序编程接口(API)之下,而应用程序层位于API之上,并通过调用API中的函数与硬件进行交互。硬件抽象层(HAL)可以很好地解决软件和硬件的“冲突”。

所以:

  • 使用HAL可以帮助开发人员减少开发时间并提高代码架构质量;
  • HAL的实现可以作为动态库或模块加载,实现了对硬件的抽象,同时也可以隐藏代码(便捷性+安全性)。

HAL实例

HAL架构

这个例子参考高焕堂的HAL框架API介绍[2]。

在Android的HAL框架使用通用的321架构,也就是三个结构体两个常量一个函数。所有的硬件抽象模块都遵循321架构,在此基础上扩展自有的功能。(以下代码由C实现)

三个结构体 (基类)

/**
* Every hardware module must have a data structure named HAL_MODULE_INFO_SYM
* and the fields of this data structure must begin with hw_module_t
* followed by module specific information.
*/
typedef struct hw_module_t
{
    uint32_t tag;
    uint16_t version_major;
    uint16_t version_minor;
    cost char* id;
    const char* author;
    struct hw_module_methods_t* methods;
    void* dso;
    uint32_t reserved[10];
}hw_module_t;

/**
* Create a function list
*/

typedef struct hw_module_methods_t
{
    /** Open a specific device */
    int (*open)(const struct hw_module_t* module, const char* id,
                struct hw_device_t** device);
} hw_module_methods_t; 

/**
* Every device data structure must begin with hw_device_t
* followed by module specific public methods and attributes.
*/
typedef struct hw_device_t
{
    /** tag must be initialized to HARDWARE_DEVICE_TAG */
    uint32_t tag;
    uint32_t version;
    struct hw_module_t* module;
    uint32_t reserved[12];
    int (*close)(struct hw_device_t* device)
}hw_device_t; 

两个常量

/**
* Name of the hal_module_info
*/
#define HAL_MODULE_INFO_SYM HMI
/**
* Name of the hal_module_info as a string
*/
#define HAL_MODULE_INFO_SYM_AS_STR "HMI"

一个函数

公共API,根据module_id去查找注册相对应的硬件对象,然后载入相应的HAL层驱动模块的so文件。

/**
* Get the module info associated with a module by id.
* @return: 0 == success, <0 == error and *module == NULL
*/
int hw_get_module(const char *id, const struct hw_module_t **module);

使用HAL操作LED驱动

根据基类设计LED驱动子类

typedef struct led_module_t
{
    hw_module_t common;
    int status;
}led_module_t;

typedef struct led_device_t
{
    hw_device_t common;
    int (*set_on)(led_device_t* dev);
    int (*set_off)(led_device_t* dev);
}led_device_t;

static int led_open(const hw_module_t* module, const char* name, hw_device_t** device)
{
    led_device_t led_device;
    led_device.common.tag = HARDWARE_DEVICE_TAG;
    led_device.common.version = 0;
    led_device.common.module = module;
    led_device.common.close = led_device_close;
    led_device.set_on = led_set_on;
    led_device.set_off = led_set_off;
    *device = (hw_device_t*)&led_device;
}

static int led_device_close(hw_device_t* device)
{
    led_device_t* dev = (led_device_t*) device;
    if(dev) free(dev);
    return 0;
}

static int led_set_on(led_device_t *dev) 
{     
    //call to led HAL-Driver     
    LOGI("led_set_on");     
    return 0; 
}

static int led_set_off(led_device_t *dev)
{
     //call to led HAL-Driver
     LOGI("led_set_off");
     return 0;
}

构造Methods代码

  • 创建methods函数表
  • 创建module对象
  • 设定open() 函数指针
static hw_module_methods_t led_module_methods =
{
    .open = led_open,
}

const led_module_t HAL_MODULE_INFO_SYM =
{
    .common =
    {
        .methods = &led_module_methods,
        .tag = HARDWARE_MODULE_TAG,
        .version_major = 1,
        .version_minor = 0,
        .id = LED_HARDWARE_MODULE_ID,
        .name = "led HAL module",
        .author = "Balabala",
    },
    .status = -1,
}

上层程序调用HAL

static int load_led_interface(const char *if_name, audio_device_t **dev)
{
    const hw_module_t *mod;
    int rc;
 
    rc = hw_get_module(LED_HARDWARE_MODULE_ID, if_name, &mod);
    ALOGE_IF(rc, "%s couldn't load LED hw module %s.%s (%s)", __func__, 
             LED_HARDWARE_MODULE_ID, if_name, strerror(-rc));
    if (rc)
    {
        return -1;
    }
 
    rc = mod->methods->open(mod, LED_HARDWARE_MODULE_ID, dev);
    if (rc)
    {
        return -1;
    }
 
    return 0;
}

以上的HAL实例是在广泛应用于 Android 中的HAL架构,开发者可以根据自己的实际项目需求进行自己项目的HAL设计。采用类似这种思路,我会在后续文章中解释我是如何学习/设计自己的HAL。

最后

最早使用HAL的经历是使用 ST 的 CubeMX 的 HAL 库,但是并没有深入了解 HAL 的设计意图和具体实现方法。其实,HAL 的设计在工程上有十分重要的意义,所以借此机会整理相关内容也希望加深自己的理解。

在 HAL 的设计过程中,我更加体会到抽象封装这两个概念在软件设计中的重要性。我们在阅读或者使用他人的代码时会有十分直观的感受,“好”的代码给人的感觉总是 simple and intuitive。汉语里有一个词叫做“见微知著”,类似的一句英文可能更加直观表示出这种感觉,叫做“see the forest for the trees”,“forest”未尝不是对“trees”的一种抽象。抽象其实就是提供了一个类似的思路,从各种“微”的硬件寄存器,到“著”的上层API,充满着“大”和“小”的博弈。抽象这种方式或者说能力,更加方便我们大脑对事物的理解与认知,更符合我们的逻辑,也能让我们的工作也好生活也好更加有条不紊。

我想用一个具体的例子展示“抽象”的魅力:比如理解计算机是如何工作的。我们都知道CPU是一台计算机的“心脏”,但如果把整个CPU拆成无数个晶体管摆在我们的眼前,恐怕冯诺依曼也要挠挠头。我们都知道CPU最基本的单元是晶体管。利用晶体管能够控制电流通断(on/off)的特性实现对电流的控制,从而得到 true/false,这些二进制的“0”和“1”就构成计算机运行的基本单位。一个“0”或者“1”在计算机中被叫做一个 bit(位)。

  • 由晶体管向上抽象,我们可以得到一些基本的逻辑门电路,包括与、或、非等基本的逻辑门实现对一个 bit 的逻辑运算。
  • 使用多个逻辑门电路向上抽象一层,我们可以得到一个全加器,实现对两个 bits 的求和运算。
  • 使用多个全加器再向上抽象一层,我们可以得到ALU(算术逻辑单元)实现对多个 bits 的算术运算和逻辑运算。

但是只是有运算能力还不够,我们需要一个地方去存储这些运算数据。

  • 因此同样从逻辑门电路向上抽象,我们可以搭建一个简单的锁存器触发器,实现对1个 bit 的信息保存,也就是可以保存一个 bit 的电平状态。
  • 由多个锁存器,再向上抽象我们可以得到一个寄存器,实现对多个 bit 二进制数据的存储。
  • 由多个寄存器再向上抽象我们就可以得到内存(memory) 也叫随机存取存储器(RAM)。

就这样,我们就由一堆晶体管创造了CPU的两个重要成员,ALU 和 Memory。他们加在一起就构成了可以运算与存储信息的CPU。用一个图片展示大概是这样。

所以,抽象是一个很神奇的概念,既可以把简单的事情变复杂(比如单个晶体管不能运算,但由大量晶体管组成的CPU可以),又可以把复杂的事情变简单(比如使用简单的指令可以让CPU运算,而不需要用复杂的指令操作每一个晶体管)。

生活中、工作中很多的事物都有着相似的 pattern。不管是软件设计中的抽象,还是“以小见大”“见微知著”这些耳熟能详的词语,很多概念或者知识就像一座座山峰一直存在着。而科学仿佛是一根环绕在我们周围隐形的线,将不同领域甚至不同维度的山峰巧妙连接,并引导着我们攀上一座山,看到下一座。朋友们,期待在科学的路上与你相遇。

to be continued…

参考

[1] https://en.wikipedia.org/wiki/Hardware_abstraction

[2] https://edu.51cto.com/center/course/lesson/index?id=41601

[3] 【Android】HAL层浅析_冇二哥的专栏-CSDN博客?