单片机的行业怪病

Posted on December 11, 2023

从单片机的存储空间说起

别说市面上没有几百家也与几十家的单片机品牌。就是同一个单片机厂家,其单片机型号也是十分繁多的。

这些名目繁多的单片机型号,归根结底,是 核心性能,存储容量,和封装引脚 数量的排列组合。

其中,存储容量是极为关键的区分价格的因子。核心性能,都会划分为 低性能,主流性能,高性能。存储容量也会分成小容量,中容量,大容量,超大容量。

但是,即使是低端低性能单片机,如果搭配的是个大容量乃至超大容量的存储空间,售价一样就会高高在上,超过高性能小容量的单片机。

而仔细品味品味不同容量之间的售价差距会发现,单片机的存储空间,是按每 KB 多少块钱算的。

在 PC 行业,一块钱已经可以买到数个GB存储容量的当下,单片机还将 KB 卖到数元的高价。存储容量的价格差距达到百万倍以上。

不思进取的单片机业界

老中医曾经说过,如果现在还要造 8GB 容量的机械硬盘,其成本并不会比 8TB 的低多少。所以,如果因为某些特殊原因,实在用不上超过 8GB 的存储空间, 使用PC进行项目开发的公司,依旧会选择1TB或者2TB的硬盘容量。并不是因为他需要那么大,而是因为当下这么大的最便宜。

但是,在单片机这个行业,情况发生了不一样的变化。如果你能够把你的代码裁剪到 8KB 大小,单片机厂就敢造 8KB 容量的单片机。 虽然他造8MB的成本和8KB并没有太大的区别。但是因为业界用不到8MB,他就会继续按8KB的目标市场造。并且为16KB的容量收取额外的溢价。

而单片机市场的受众,也被这种吃老本的源头所驯化。特别在意代码的裁剪。于是二者是相互成就。一直压着单片机的容量。在30年的时间里,单片机的主流容量,只从 8KB 增长到了 128KB。 增加不过 8 倍。

而同期的 PC ,存储容量则从 1.44MB 暴涨到 8TB。增长超过五千万倍。

整个单片机业界陷入一终迟滞。不仅仅厂家喜欢躺着吃老本。顾客也要求厂家吃老本。

曾经有人说,某单片机厂家非常良心。一款单片机卖了十几年还在供货,而且没涨价。他是想跨人家供货周期长,对自己的项目后续供应有保障。还夸人家十几年不涨价,保障了用户的利益。

我的天,如果 intel 现在还在按同样的价格卖 80386, 不知道他还会不会夸 intel 业界良心。

但是即使是同一个人, 他还是会骂 intel 是牙膏厂的.

那么为何会这样呢?

还是代码出了问题

按理说, 顾客是不喜欢吃老本的商家的. 谁都希望花同样的钱买到的东西越来越好. 但是单片机程序员为何是个例外?

因为, 如果老款停产了,新款性能再好, 对单片机程序员来说都是没有用的.因为他的代码跑不了.

单片机的程序都是为特定的单片机完全适配开发的. 单片机不仅不能升级增加功能, 连完全不增加新功能只是单纯的提高主频, 都是不可以的. 因为单片机代码里大量的 delay() 函数已经写死了主频. 更换主频就会导致 delay 时间变短. 于是原来设计好的逻辑就无法正常运行了.

于是为了适配新款单片机,就得对代码进行修改. 修改完毕后要重新走测试验证流程. 这些都会额外的增加项目的开发成本.

单片机码农为了吃老本, 也就逼迫上游厂家吃老本了. 而上游厂家吃老本, 也逼迫新入行的程序员吃老本. 相互影响相互锁定.

所以, 破解这个死结, 首先要求程序员自我进步. 让单片机项目彻底和具体的某个单片机型号脱钩. 也就是要编写和硬件平台无关的代码. 把与硬件平台相关的代码最大限度的限制在一个非常小的范围.

要想达成这个目标, 就得选择一个更容易达成这个目标的好语言. 所谓工欲善其事必先利其器. 一个语言,必须要具备良好的抽象能力.

所以,选择单片机开发, 语言的第一个要求, 就是要支持更高层次的抽象能力. 所谓更高层次的抽象能力, 说人话, 就是要支持 “面向概念编程”。

面向概念编程, 具体到某个具体的语言里, 有不同的叫法。C 语言完全不支持。 所以 pass。

C++ 对面向概念编程的支持所提供的工具就是 c++20 提出的新特性: “concept”。

另一个具有良好抽象能力的语言,我叫他 javascript, 或则 nodejs。

除了要支持良好的抽象能力,性能也是不得不考虑的因素。 所谓的语言的性能,指的是编译器将高级语言转换为机器指令的时候,相比人肉写汇编指令多多插入的代码。 语言的性能越高,说明编译器生成的代码就越是精炼。

很多时候,语言对性能的影响是非常小的。主要还是看编译器的优化能力。

但是也有例外。编译器的优化能力是首先不能违背语言的定义。

比如,C 语言规定 除 0 是一个未定义行为。 于是编译器遇到你代码上写着 a / b; 就会直接生成硬件的 div 指令。 至于遇到 除 0 , cpu 会发生什么,编译器也不关心。因为 cpu 发生什么行为,都符合 C 语言标准的定义。那就是未定义行为。

但是,如果一个语言规定了,除 0 必须要有某种具体的后果。那么,在 cpu 执行 除法指令的行为和语言规范不一致的情况下,编译器必须要插入除数是否为0的检查。如果为0,则执行语言标准所定义的行为。于是你写了一个 a / b . 编译器却生成了大量的检查 b 是否为 0 , 并且为 0 后要干什么的指令。

那么,这种语言,编译器无论如何改进他的优化算法,都不能比C更高效。

这就是为何,很多程序员诟病 C 语言未定义行为太多。但是最终他们选的任何语言都不可能比C更高效。

所以,一个能用在单片机开发上的语言,必须比 C 语言更高级,有更高的抽象能力。但是,同样也要比 C 语言更高效。

恰巧, C++ 语言就是那个 0 开销增加高级抽象能力的, C 的 改进改进版。

什么叫 0 开销抽象

C++ 把他高级的抽象能力叫做 0 开销抽象。为何叫 0 开销抽象呢?

所谓 0 开销抽象,是说如果你不用 C++ 提供的高级抽象的功能,而是使用 “手撸“ 的方式达成。那么你手写的代码必然不可能比编译器自动生成的代码更高效(开了编译优化的情况下)。 比如你用函数指针的方式实现 虚继承 法的多态。并不会比直接使用虚函数更快。

c++ 其实对于0开销的说法,是过于谦虚了。 c++ 的许多高级抽象能力,不仅仅不是 0 开销,还是负开销。

因为会比程序员手撸的方式更快。

比如模板元编程。让很多计算行为发生在编译期。这在 C 语言里用多少 宏 定义都做不到而不得不放在运行时运行的代码,在C++里,可以用模板元编程在编译期完成计算。

举个最简单的例子,处理正则表达式。

在 C 语言里,正则表达式是一个字符串。正则表达式引擎会在运行时的时候解析这个字符串,然后把这个字符串编译为一个字节码。这样后续用这个正则表达式匹配字符串的时候,就会非常高效。

但是,在 C++ 语言里,正则表达式可以在编译期就被直接转换为机器代码。做到这点靠的就是模板元编程。

而为了实现这点,需要使用模板元编程的方式写代码。代码就会显得不是特别直观。

为了解决模板元编程的代码不直观的问题。 C++ 又加入了 constexpr 和 concept 大杀器。

很多不理解的人,都会认为 C++ 的功能越来越多,越来越膨胀了。

但是理解的人就知道, C++ 所有的新功能增加,都是为了服务一个目的,就是为了更好的 0 开销抽象。

C++ 不仅仅没有因为功能越加越多而越来越慢,反而是因为 0 开销抽象能力越来越强而让代码变得越来越快。

比如右值引用,就减少了大量的毫无意义的拷贝构造。单纯的将原来的老代码用支持 c++11 的编译器重新编译都能获得速度提升。

接口设计能力

完成同样的事情,其实可以设计很多种不同的接口。虽然最终都是殊途同归。但是也分易用和不易用。

就拿常见的 0.95寸的 OLED 屏幕来说,把这个屏幕封装成一个易用的接口,就有多种方式。

最常见的方式,是把这个屏幕封装成一个 iostream 流。用 printf 的方式使用。

其实还有一个封装方法,就是当成字符阵列。比如想在 第一行第三列输出一个字,可以这样写

oled[1][3] = 'H'

如果是在第2行第4列开始输出一行字,可以这样写

oled[2][4] = ”一行字“

如果是按像素,则可以依葫芦画瓢。因为 c++ 接受 运算符重载,于是可以通过重载 operator [] 实现按行列定位。 又因为 c++ 支持按函数参数重载。于是可以通过重载 operator = (各种类型) 实现输出不同类型的内容。文字,数字,图片都可以。

又因为 operator [] 也可以按不同类型的参数重载。 于是可以重载 int 参数,用于按文字大小的行列定位。 如果用 px 类型定位,就按像素。而且 px 类型,又可以使用 c++ 的 user defined literals 实现如下的用法

oled[3px][0px] = RED;

最终,这些操作都被转换成 spi或者 i2c 命令发往 oled 屏幕。

按日此方式做的接口,就可以说是非常的易用了。而且有利于用户在编写 GUI 代码的时候非常直观的明白输出效果。

而如果使用 C, 是没有这些能力的。

如果还是用 C 时代的方法设计接口,就容易只是看起来像是多了个对象,实际上还是 C 。

表扬 ESP32

在一众以 KB 算容量的单片机里, ESP32 鹤立鸡群的给以 MB 来计算容量的单片机。而且还比这些单片机更便宜。

而且率先以 c++ 作为开发语言,吊打一众只知道 C 和汇编的老厂。

Comments