月度归档:2020年09月

浅谈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

张三的财富自由之路

2020年的某天清晨,张三从梦中惊醒,总觉得有什么东西从他的脑中潜进来又溜了出去。环顾四周,今天还是和往常一样,是新冠疫情魔幻一年中的平常的一天。张三这时发现了脑中奇怪的感觉来自哪里,应该是他昨晚做的梦。梦中的张三变成了一个有钱人,有着比尔盖茨一般挥之不去的巨额财富。然而醒来后他渐渐意识到,虽然他和比尔盖茨的平均身价有超过500亿美元,但是比尔盖茨的个人资产就已经超过1000亿美元,也就是说他的财富体量在两个人的财富总值中应该算是“微不足道”的小。然而巨大的落差并没有将张三击倒,他用力地摇了摇头,想要努力确认自己到底是否还是在梦境中,自己到底是了不起的“比尔盖茨”还是那个马上要上班出售自己时间的人。在确认自己已经离开梦境回到现实之后,张三终于意识到他离财富自由距离最近的时候,是在他的梦里。

财富自由,是极具诱惑力的一个词,也是极具有迷惑性的一个词。不同的人对“自由”的标准不同,有的人认为拥有1000万可以财务自由,有些人认为需要10个亿,还有一些人认为可能只要够生存用就是一种自由。虽然张三不是一个财富自由的人,但是并不妨碍张三对自己的资产情况产生好奇,自己到底有多少资产呢?他突然想到了公司的“财务报表”,那么自己可不可以有一份属于自己的财务报表呢?

什么是财务报表

财务报表(Financial statements),对于一个企业十分重要,目的是提供有关企业财务状况,绩效和财务状况变化的信息,这些信息对于广泛的用户进行经济决策很有帮助[1]。也就是可以反映出一个企业的财务状况和经营状况,炒股的人想必对这个概念并不陌生。那么对我们个人而言,财务报表又有什么帮助呢?如果你是一个习惯记账的人,应该能够体会到这种财务管理的方式的重要性。我们自己的财务报表能够直接或间接地反映出我们的资产、负债、收入等等信息,也能够帮助我们更好地思考与管理我们的财富。

即使收入不高或者存款不多,记账也是一种管理自己的财富的很好的习惯,觉得记账很琐碎的话也可以整理一下每个月的信用卡账单。因为,有些事情我们往往在参与进去之后才会去更加主动地思考。当我们培养出对“财务”这个概念的敏锐度的时候,我们会逐渐习惯思考如何“开源”,如何“节流”,如何用投资等方式增加自己的财富。而培养出的这种对财务的敏锐度(financial acumen)就是我认为的思考这一些列问题的目的,也是向“财富自由”更近一步的一种努力。

财务报表,通常包含四项基本报表[2]:

  • Balance sheet 资产负债表 – 报告给定时间点的公司资产,负债和所有者权益。
  • Income statement 损益表 – 报告公司在指定时期内的收入,支出和利润。 损益表提供有关企业运营的信息。 这些包括销售和在规定期间内发生的各种费用。
  • Statement of changes 财务状况变动表 – 用于报告指定期间内公司权益的变化。
  • Cash flow statement 现金流量表 – 报告了公司在一定时期内的现金流量活动,特别是其经营,投资和融资活动。

这里主要介绍资产负债表、损益表、和现金流量表这三项。从资产负债表中我们可以看到一个公司是不是有潜力,面临哪些风险,又有哪些机会。从损益表我们可以看到一个公司现在赚不赚钱。从现金流量表我们可以看到一个公司的现金流,也就是类似行军打仗的后勤能力。

资产负债表 Balance Sheet

  • 显示特定时间的财务状态
  • 类似一张照片直观展示出财务状态
  • 能了解到有哪些资产,以及哪些负债

资产:流动资产(比如现金、股票)+ 非流动资产(比如固定资产、房子等)
负债:短期和长期的贷款,债务,所有者权益(stakeholders’ equity)

损益表 Income Statement

  • 显示企业一段时间的收益或损失
  • 能不能赚钱的能力
  • 类似一段影片记录这个过程中的种种行为

现金流向表 Cash Flow Statement

  • 统计一段时间的现金流动
  • 类似一段影片记录这个过程中的种种行为
  • 现金流 = 流入现金 – 流出现金
  • 包括:运营活动现金流、投资活动现金流、财务活动现金流

整理成表格:

张三的财务报表

拿着公司的财务报表,张三开始思考起自己的报表会是什么样子。

张三是一名有志青年,每月工资1万不高不低,靠着自己的努力几年的时间辛苦攒下了35W,2019年勉强买了一套房价值100W的房子用于投资,首付30W,贷款70W,利率5%,三十年偿还,每个月需要还3757。但是还好目前张三是单身,所以并没有很大的家庭压力,张三希望能知道自己的一些财务状况,怎样才能增加自己的财富,并且希望早日升职加薪,迎娶心中的女神。

根据张三的实际情况,2019年张三的大致财务报表大致是这样:

所以,张三2018年拥有35W的存款,虽然2019年存款只有9W多,但是净资产达到了60W。主要的财富增长小部分来自于工资收入,大部分来自投资收益,也就是房产增值收益。如果张三运气足够好,每年可以达到10%的净资产增值,那么10年以后张三的净资产会超过155W (假设张三的工资没有增长,也没有通货膨胀)。

所以如果张三能够保持可观的收入,并找到适合自己的投资渠道,或者在今年年初买了特斯拉的股票,那么张三可能会更快达到155W的财富,也就离自己的女神和财富自由更近一步。

在公司门口,信心满满的张三拿着自己新鲜的“财务报表”,仿佛看到了几十年后自己的美好生活,不禁握紧了拳头暗自打气,同时迎上了不远处女神的目光。站在女神旁边刚刚从老爸那里拿了玛莎拉蒂的李四觉察到了张三的目光,也抬起了头,向张三浅浅一笑,三人间的气氛顿时变得有些微妙。

参考

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

[2] “Presentation of Financial Statements” Standard IAS 1, International Accounting Standards Board. Accessed 24 June 2007.

浅谈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博客?

为什么我们会选择创造一个新的生命?

今年1月8日,我们家迎来了一个新的小生命,我的儿子小八出生了。多了一个爸爸的身份之后,我的生活发生了巨大的改变,这也引发了我的一些新的思考。这篇文章尝试讨论有关生育的一些原因,生育对于家庭的影响,以及育儿和二胎的相关话题。

为什么要生宝宝?

关于为什么要生育,我通过从网上的搜索和周围人的回答得到以下几点主要原因:

  • 意外怀孕
  • 养儿防老
  • 有了宝宝会使人生更加完整,增加生活的乐趣
  • 为了不让自己后悔
  • 就是自然单纯地想要生,没有什么原因
  • 生育是一种延续生命的方式,是一种责任,有更多的家庭和长辈方面的要求

由此可见,人们生宝宝的原因多种多样,那么生育这件事情对我们而言意味着什么呢?现在的经济、生活压力这么大,我们到底应不应该生孩子呢?

《妇女权益保障法》第51条规定:

妇女有按照国家有关规定生育子女的权利,也有不生育的自由。

所以,生孩子本应该是一件完全自由的事情。即使家里的老人有一万种你应该生孩子的理由,生或者不生,都应该由夫妻两个人共同所决定。我一直很重视父母在育儿上的参与感,坚持孩子的父母只能有一位,也就是只能有一个爸爸和一个妈妈。听起来也许有点荒唐,但这里我是指对于一些主要由爷爷奶奶或者外公外婆带大,同时父母很少参与育儿的孩子,爷爷奶奶或者外公外婆就是他们心中的的“爸爸”和“妈妈”。称呼上的区别并没有那么重要,这些宝宝还是会叫爸爸妈妈,会叫爷爷奶奶、外公外婆,但是心理上他们的情感往往会更加认同带自己长大的老人而不是生物学意义上的所谓父母。

所以我的原则是,家里的老人可以帮忙照顾孩子,但是一定是帮忙,而不是有帮我照顾宝宝的义务,也不能代替我们作为父母的作用。所以,如果你是因为家里老人的要求生了一个宝宝再给老人去养大的话,我会觉得这是对自己的家庭和宝宝都很不负责任的行为。当然,每个人每个家庭的条件和环境不尽相同,也许我不应该随意地judge每个人的选择。但是,从个人情感出发,为了自己舒适享受,不想操心劳累,追求所谓的自己的“事业”,而把孩子放在老人身边不管不顾的行为是令我十分反感的。所以,在了解自己的时间、经济情况,年龄身体情况之后,我鼓励夫妻共同决定是否要生育。不管推迟生育还是不生育,都要经过负责的思考并尊重配偶的意见。而一旦夫妻做好准备生育的决定,也请今后尽最大的努力去参与育儿这个过程。

关于为什么人们会想要生育还有一个比较有趣的观点。在《自私的基因》中, 道金斯强调我们都是“自私的基因”的载体(survival machine),目的就是能够最大程度地保存与复制我们的基因。人们的“无私”的行为,仅是从宏观上的无私或者利他行为(altruism),而从基因的层面看这些行为其实是“自私”的。比如,母亲会为后代无私提供食物,甚至可以为保护后代牺牲自己,这其实是是一种基因层面的“自私”的行为,目的是能够让自己的基因更好地保存和延续下去。

那么在生物学的角度,我们是不是可以认为生育其实是一种基因写在我们身体中的某种“需求”,让我们都会有想要生育的想法。只是,有些实际情况会导致我们拖延或者完全拒绝这种想法。道金斯甚至在书中提出,通过繁殖,基因可以实现在某种意义的永生(immortal)。关于这一点有一些争议,因为“永生(immortal)”这个概念源于宗教。人们会死亡(mortality),尸体也会腐败。所以相对于人有mortality这个特点,宗教提出了上帝是immortal的这个概念,因为上帝并不存在于某个具体的身体中,所以自然不存在物理上的死亡。

Immortal和mortal的定义是相对的,但这种mortal or immortal 的定义却并不对基因适用,我们不能说某个基因是否是“活着的”,因为基因只是生物学意义上包含遗传信息的一种物质,随着生物体的消亡,相应的基因自然也就烟消云散。道金斯所指的“immortal”的基因应该指的是基因可以通过不断地复制,交换,分裂保存下这种pattern。但是,使用并强调“immortal”这种说法难免有一点诱导人们往哲学上面去思考,而不仅仅是从科学的角度上去解释生物繁衍的过程。

回到生育的选择问题,以上都是一些可能影响生育选择的因素,不同的人会因为不同的原因选择是否生育。而在实际生活中,选择不生育的人要远少于选择生育的人,所以正常情况下,我们的家庭中都会有或者将要有一个或者多个宝宝。那么,宝宝是如何影响我们的生活,我们又应该做一些什么呢?

宝宝对家庭的影响

对于一个父亲,养育一个宝宝可能要面临的挑战包括[1]:

  • 身份和责任的改变 – 从工作、生活、休闲到增加抚养孩子多带来的的更高的责任感;
  • 和宝宝的情感连接 – 了解并走入宝宝的世界往往需要花费很多时间,每个爸爸的耐心和能力也都会有很大的不同;
  • 和伴侣的关系 – 可能会产生新的优先级,并且和伴侣产生分歧发生矛盾;
  • 和伴侣家庭的关系 – 如何处理好婆媳关系,处理好和岳父岳母的关系往往是个难题。

以上所有的挑战,对于妈妈同样存在,并且相比与爸爸们,妈妈们往往背负了更多的家庭和社会的要求和期待。爸爸们体会不到分娩的痛苦,生育后可能产生的产后抑郁的痛苦,母乳喂养的痛苦,堵奶涨奶的痛苦,以及由于社会和家庭的不合理期待所导致的不公平的心理和精神压力。所以,我经常建议周围的爸爸们要多关心妈妈,多承担照顾宝宝的责任。虽然我本人也不敢说做得十分优秀,但是我会时刻保持这种意识,不断地学习育儿知识,积极和队友配合,希望能尽最大努力做好自己、爱护伴侣,给宝宝一个温馨有爱的成长环境。我相信这也是每位爸爸所希望的。

但是现实情况却并不是这样。因为有宝宝的原因,我加了爸爸交流群,老婆加了妈妈交流群。同样目的的两个群,群里的内容却天差地别。妈妈群一般都在积极地讨论宝宝的各种问题,交流彼此的经验和建议,偶尔吐槽老人们带娃的旧观念。而爸爸群对宝宝的讨论相对少很多,并且有些爸爸甚至会在育儿群中“开车”,发黄图,讲低俗的段子,开低俗的玩笑。这样的爸爸肯定是人群中的少数,但是能在育儿群中有这样的行为还是让我大跌眼镜。我并不觉得是素质这样简单的原因导致了妈妈群爸爸群的差别。我从这个事情可以明显感受到,妈妈们更关心自己的宝宝,而大部分爸爸们并不是很在意育儿这件事情,或者更多地觉得育儿更大程度上是母亲的责任。

归其原因,也许还是社会对母亲这个角色在育儿上的要求更为苛刻。“为母则刚”是十分刺眼的几个字,难道当了妈妈以后就只能“刚”了吗?否则就是对家庭和孩子的不负责任?我并不是个女权主义者,但也会觉得这种想法荒谬得可笑。现在越来越多的女性拥有保护自己权益的意识,究竟是社会的进步还是退步呢?作为社会的“中流砥柱”,男性朋友又是否平衡好了社会责任和家庭责任呢?这值得我们反思。

关于二胎的选择

现在的政策已经允许并且鼓励家庭生育第二个孩子,有一些家庭也开始在是否要生二胎这个问题上进行选择。关于子女的出生顺序的影响,个体心理学的提出者阿德勒在《自卑与超越》中提出过这样的看法:独生子女很容易被溺爱和娇惯,以自我为中心,长大后对待挫折的能力就会更弱。如果是多子女家庭,最大的孩子,在老二出生时,都会有一种丧失地位的经历,这种经历会让他们成年后倾向于保守主义,长大后,会通过打造自己的稳固地位,比如说过度依赖规则和法律,来弥补心中的自卑感。而家里的老二,而天生更适合合作,因为一开始在他们前面就有一个领跑者,所以他们长大后,也善于去挑选一个有优势的人来跟自己比较,然后努力的超越他,而最小的孩子呢,由于有太多的竞争对手,会比其他孩子进步都快,发展更好,但是最小的孩子也是问题儿童的高发群体,因为身边的每个人比他们看起来都更强大,更有经验,这种时候就容易产生严重的自卑感[3]。

所以,并没有一个所谓的子女数量的最优解,更重要的是要保持子女的自卑感与优越感的平衡。自卑感可能来自于原生家庭的忽视,亲情的缺失,而优越感往往来自于对孩子的无条件宠溺。自卑感或者优越感并不是总是坏的事情,适当的自卑感会驱动孩子的成长,而适当的优越感则有助于孩子树立自信。所以,相比关心要生育的孩子数量,思考如何保证养育每个孩子的成长质量更为重要。比如个体心理学中强调的提高孩子的合作意识和社会责任感就是一个很好的方向。因此,根据家庭的真正实际情况,从经济、精力、意愿等方面考虑,选择是否要生二胎会更加现实可行。比如,对于我的家庭,由于我们夫妻的精力有限,以及对婴儿早期哺乳的“恐惧”,我们的决定是并不打算生二胎。

最后

人们可能会因为各种各样的原因选择生育或者不生育,可能是“自私的基因”的影响,可能是各种客观环境的影响。但是,不管怎样,从这个注定与我们紧密连接的生命诞生的那一刻,我们就拥有了一个新的身份:父母。随之而来的还会有最初强烈的不适应感,令人感到压迫的责任感,以及失去自我的迷失感。第一次为人父母,难免会有各种的迷惑、犯错、手忙脚乱。做好充分的知识积累的同时我们也要允许自己犯错,不必因为一点错误过于苛责自己,而要从错误中从中吸取经验。育儿的过程,也是我们父母镜映自己一起成长的过程。

与此同时,也请不要因为有了新的家庭成员而忽视、冷落了和伴侣的亲密关系。虽然动物的本能或基因的“控制”让我们格外关注我们的后代,但是作为人类我们有着区别于其他动物的强大的认知与思考能力,我们有能力通过我们的大脑了解、控制并保证对配偶保持足够的爱。

不论家里有一个或者几个宝宝,保持对每个宝宝的足够关心都很重要。我们都知道对待孩子不能太过严厉,也不能太过溺爱。但是如何平衡好对孩子的严厉和温柔,让孩子不会产生过度的自卑感或者优越感,真的是个难题。育儿这个过程充满了未知,也因此才让我们拥有很多期待。作为走在“父母”这条道路上的新成员,我深知道阻且长,但还好我并不是孤身一人,而是能与我的另一半一起在这段注定不太平坦的路上相互扶持、笑对起伏、共同前行、欣赏途中有彼此的风景。

Happiness is true only when shared. My son, dad is always happy to share all the up and downs in my life with you and mom.

参考

[1] https://www.cope.org.au/family-community/fathers-partners/impacts-life/

[2]《自私的基因》https://book.douban.com/subject/1292405/

[3]《自卑与超越》https://book.douban.com/subject/3389960/

Infotainment: 关于车载音响和驾驶体验

不知道人们在坐车、开车的时候有没有考虑过车上的音乐是如何播放的,我在没有做这行之前是没什么概念的。所以当我知道跑车的轰鸣声不是引擎的声音,而是由汽车喇叭模拟出来的时候感到很是震惊。这篇文章内容大部分来自于公司的培训内容和工作中的总结,整理了一些基础音频概念以及一些汽车音频相关的基本知识,算是一种入门介绍和知识梳理。

车载音响(Car Audio)是如何工作的

汽车影音系统最重要的几个组成部分:

机头(Head Unit) 主要在汽车的中控,是整个影音娱乐系统的控制中心。

功放(Amplifier) 一般位于汽车后部,起到对音频信号的boost和amplify的作用。一些酷炫的算法,比如让只有驾驶员能听到音响声音从而实现对乘客的免打扰,或者主动降路噪等算法也是在功放中实现的。

喇叭(Speaker) 遍布于驾驶室周围,有些车型还会有将喇叭安装在车轮中。这些喇叭实现了音频信号的输出。

总线(Bus) 包括基本的CAN控制总线,以及MOST,A2B,eAVB等总线传递媒体数据。

在车内的分布如下图所示。

一些基本音频概念

音频(Audio),是与人类听觉系统的可听范围内的信号有关的对象(例如,信号,过程,设备,系统)或特征(例如,频率,水平,时间)。

声音(Sound),描述了通过空气或其他介质传输的音频信号刺激个体听众的人类听觉系统而产生的主观听觉。

音质,主要指的是Sound Quality.

[公式]
[公式]

关于音质不同的人有不同的喜好和标准,以拍照为例,有人喜欢iPhone原图,有人喜欢美图秀秀。有人喜欢真实不加修改的,有人喜欢美化后看起来赏心悦目的。

功放工程师更像是一个摄影师,而不是化妆师或者模特,我们需要最大程度保留音频的信息,并且根据不同的需求对音频进行加工,输出用户满意的音质。

Limitation:音频限制,包括信号的幅值,频率,时间等。

Unintended change: 由音频系统中的意外过程引起的音频信号变化。

Intended change: 由音频系统中的预期过程引起的音频信号变化。

THD+N: total harmonic distortion + noise, 总谐波失真。

Frequency response: 频率响应。

SNR: signal to noise ratio, 信噪比。

Crosstalk: 声道之间的音频信号耦合电平。

PSRR: power supply rejection ratio, 电源抑制比

音频性能标准

频率响应

人们可以听到的音频范围是20Hz – 20KHz。 好的音频期望实现的目标是该频段的频率响应非常的平坦。

例如,将1KHz的电平设置为0dB,20Hz – 20KHz频带中的最大和最小电平应不超过允许的相对电平限制(例如±1dB)。

当输出功率改变的时候,频率响应可能也会受到影响。所以,对于不同的输出功率,相应的频率响应曲线都应该满足设计要求。

输出功率

功放的输出电压不能超过其电源电压,否则会削波。削波意味着高失真(THD+N),所有功率计算毫无意义。

只有有足够的增益才能达到最大输出功率。

  • 增益太低 – 即使最大输入电平(最大音量)也无法达到最大输出。
  • 增益太高 – 削波太早发生,数字信号动态范围受到限制。
功放的传递曲线

THD

  • 根据傅立叶展开,正弦波在频域中具有一条谱线。
  • 正弦波的任何失真都会产生意外的谐波。
  • 谐波电压与信号电压的RMS之比表示失真程度。
  • 系统失真特性也可以应用于其他信号。

噪声 Noise

噪声是与输入信号无关的意外的随机输出单声道。

谐波是信号的一部分,而噪声不是信号的一部分。

Pop Noise

  • Pop noise是电源开/关,诊断,软件设置期间的意外瞬态噪声。
  • 使用不同的扬声器可能会(不会)听到爆音,没有相应的电气测试阈值。

功放 Amplifier

目的(1st):

  • 驱动需要大功率(大电流)的扬声器
  • 同时驱动多个扬声器
  • 均衡以补偿车辆内部形状和扬声器位置

目的(2nd):

  • 音频改善算法(Quantum Logic™,Logic7®,Signal Doctor™)
  • 环绕声解码(杜比音效,DTS®)
  • 车辆噪声补偿(DEC,ANC,EOC)
  • 通信总线控制(CAN,MOST)
  • 引擎声音合成(ESS)- 是的,跑车的轰鸣声不是引擎声而是喇叭发出来的.

汽车音频相关总线

  • CAN
  • MOST
  • A2B
  • IEBUS
  • eAVB

功率放大器 – Class AB & Class D

AB类放大器使用线性调节晶体管来调制输出电压。 这些设备中的损耗与通过它的电压和通过它的电流的乘积成正比。

D类放大器采用的MOSFET完全导通或截止。 理想情况下,可以实现零功耗。 在实际应用中以下情况会引起开关器件的功率损耗:1)有限的过渡速度; 2)导通阻抗; 3)栅极电荷。

电压跟踪(Tracking Power Supply)

利用这样的动态信号进行再现,可以通过在降压-升压配置中用电源跟踪音频信号来进一步减少损耗(功耗浪费)。也就是,通过低音量低电压,高音量高电压减少功耗。

疫情下的现状

由于新冠病毒的影响,全世界的汽车行业都收到了很大的冲击,所以对于车载音响无疑也是一种很大的打击。美国和欧洲的工厂停工过一段时间,导致全球车厂的产量降低。而国内市场由于新冠防护工作做得比较好,复工比较早,受到的影响相对而言没那么严重。但是整起来看,相应车载产品在第一二个季度的销售都有明显的缩减。

因此,在疫情期间做出相应的结构或者产业调整或许是一种可行的选择。因为往往这种调整会导致公司结构的变动,对产品研发、生产、销售等方面也会有比较大的影响,如果改革不当受到的打击也会很大。但是在疫情环境下,这种波动所造成的影响会比以往小一些,也就是风险系数会小一些。所以,很多公司选择在这次疫情期间开始进行一些改革和调整,也算是“不幸中的寻求万幸”吧。

对于每个开车的人来说,驾驶体验都在变得越来越重要。司机和乘客不再只满足于能听听广播和音乐,而是有了更多的影音娱乐的需求,所以car infotainment也是各个厂商目前在竞争的一块蛋糕。驾驶过特斯拉的人应该能够体会到好的infotainment对于提升驾驶体验有多重要。在疫情期间,虽然汽车市场没有那么火热,但是人们也更加倾向自驾出行而尽可能避免使用人流密集的公共交通的方式。所以对于在后疫情时代是否会有一波汽车消费的高潮,从而带动car infotainment的进一步发展,让我们拭目以待吧。