0%

1. 最美的理论 - 广义相对论

在牛顿的万有引力为主的经典物理学角度,我们认为万物皆有引力,甚至发明了“引力场”的概念来描述引力在空间中的传播,就像磁铁吸住铁一样,地球有巨大的引力让人无法飞起来,而太阳也用巨大的引力让地球围绕自己转。

然而爱因斯坦发表了广义相对论,他认为所谓的“引力场”就是空间本身,巨大的星球并不是通过自身的吸引力吸住周围的物质,而是直接扭曲了空间——物体本身还是沿直线前进的,只不过“轨道”被弯曲了。就像一张拉平的床单上放了一颗铁球,它扭曲了原本平直的床单,导致其他小的铁球不得不围着它转动。

爱因斯坦的广义相对论(时空弯曲)直接或间接导致了黑洞、宇宙大爆炸、引力波等概念的诞生以及被观测证实。

2. 量子 - 不可分割的个体

量子是一种最小的、不可在分割的物理量。就好比显示屏上的像素,它就是量子化的,一张图片的分辨率再细腻,最终还是要落实到每一个像素上(不存在半个像素)。

经典物理学认为,能量是连续的,就像你把水从0烧到100度,这个过程是平滑而连续的。然而量子理论却认为,在微观世界,能量是一级一级地往上爬,能量单位是有最小极限的,我们姑且称之为“能量包”,原子核内的电子所包含的能量只会是一个特定值——即多少个能量包,当电子的能量包达到另一个特定值后,就会立刻跳到另一个运行轨道上,这是著名的——“量子跃迁”。

量子理论的诞生颠覆了经典物理学的很多概念,在传统的科学观念里,一切都有规律,可以通过公式计算得出。然而在量子世界里,电子并不是一直存在的物质,它只有和某个物质发生作用是才会以一定的概率“现身”,否则你都无法用“存在”这个概念来形容它。

我个人的理解,这就好比影子,有光照的时候就会有影子,没有光照的时候影子存在吗?即,在量子世界,我们不知道其内部的构造,只知道它是如何作用的。

不论如何,我不是特别理解量子的概念,但是可以肯定的是,基于量子力学,晶体管问世了,进一步将人类文明推向信息时代。

3. 宇宙的构造 - 世界有多大

从人类文明开始,“世界有多大?”这个命题经历了:

世界只有天和地
世界是圆的,地球是中心
世界是圆的,太阳是中心
原来太阳只是银河系的一粒尘埃
原来还有数不清个像银河一样的星系
更多的星系和未知都包含在宇宙之中
宇宙的空间不是均匀的,而是被各种天体扭曲,像波澜起伏的海面
宇宙之外是什么?

4. 粒子 - 世界有多小

原子曾被认为是物质的最小单位,它所形成的各种元素构成了宇宙中的万事万物,然而:

原子由原子核及电子构成
原子核由更小的中子和质子构成
质子和中子又由夸克构成
夸克又被更小的胶子粘在一起

此外还有中微子、希格斯玻色子等粒子,共不到十种粒子,像乐高积木一样,以不同的组合方式,造就了万事万物。

科学家们把目前已知的这几种基本粒子的物理理论拼拼凑凑,得出了一个叫“基本粒子标准模型”的东西,用于解释这个世界。不论如何,它很有用,但显然还不够完美。我们追求的应该是一套统一且简洁的理论,而不是在这个场景下用这套公式,那个场景下用那种方法的多种理论组合。

不过近些年,这个标准模型出问题了。科学家发现在每个星系周围都存在着一团巨大的云状物,需要通过它自身的引力和对光产生的偏折现场,才能间接地“观察”到它。而构成它的基本粒子,不属于“基本粒子标准模型”中的任何一种。到底是什么,目前不得而知,科学家仅仅把它称为——暗物质。

世界还能更小吗?不知道。人类目前对物质的认知:在屈指可数的几种基本粒子,不断以存在/不存在之间切换、震动、起伏,充斥在无边无际的空间中,构成了所有我们已知的世界。

5. 空间的颗粒

  • 广义相对论告诉我们,时空是光滑且可扭曲和压缩的,这是引力场的本质
  • 量子力学告诉我们,所谓的场是由量子构成,具有精细的颗粒状

显然,前者认为世界平滑连续,后者认为世界是量子化的,二者矛盾。但两个理论在今天都非常有用,而且在各自的领域都很正确,这又作何解释?

圈量子引力试图统一二者。

圈量子引力认为:空间本身是量子化的,也就是说我们存在的空间本身也是“物质或能量”的一种,那么构成空间的最基本单位姑且称之为“空间量子”。它们比原子小几亿亿倍,外表可能像个圆圈,环环相扣,编织出了我们存在的整个空间!不理解这一点很正常,就像河中的鱼儿,也不知道水的存在。

圈量子引力还有个更极端的结论:即然空间可以被扭曲和压缩,那时间的连续性也就没有了。或者说根本就不需要“时间”这个变量。每个物理过程都有各自的节奏,有快有慢,彼此独立。

形象点说,物质的物理过程,其实是物质的量子和空间的量子发生作用的结果。比如你从A运动到B点,并不是你挤开空间量子钻过去的,而是你身上的每个物质量子与前面的空间量子作用后,位移过去的。(不知道我这样理解对不对),就像屏幕上移动的鼠标,其实是无数个像素的一次组合交替过程。

接下来的结论更有意思——黑洞。由于巨大的质量会把空间压缩到极高的“密度”,如果你身处黑洞之中,“眨眼”这个上下眼皮的空间距离,是正常世界的成百上千倍,相当于这个运动过程的“节奏”被放慢了,可谓“眨眼瞬间,世界千年”。(让我想起了星际穿越)

6. 概率、时间和黑洞的热

客观上的热

我们都知道,“热”的本质是分子运动的快慢程度。但为什么热量总是从热的地方留向冷的地方呢——概率。

想象操场上有一堆球,它们都在不规则运动,有得很快,有得很慢。从统计学的角度看,快球总是有更大的概率撞到慢球,而由于能量守恒,碰撞后二球的速度(能量)为二者的平均值。再回到宏观看,最终整个操场的球的速度就会变得一样。反过来,如果用概率来看,水烧不开也是有可能的,但是无数的快球都完美避开慢球的可能性,几乎为零吧。

客观上的时间

时间其实是人们主观上的概念,例如现在、此时此刻、刚刚、将来、一分一秒等,都是以人的主观视角来描述世界的运行过程。在物理上,这是不客观的。那物理上应该如何描述时间的流逝呢——热量的转移。

想象一个老式钟摆,你用摄像机把它摆来摆去的过程录了下来,然后不论你正着放还是倒着放这段录像,是不是没区别,因为它的过程是重复的。那又如何判断录像是正放还是倒放呢?物理上,钟摆运动会有摩擦力,摩擦生热,只要观察中摆是越来越热,还是越来越冷,就能搞清楚播放的顺序,即时间如何流逝的。(当然,搞懂道理即可,没必要在摩擦生热的问题上抬杠)

黑洞是火热的(这部分我每理解)

史蒂芬·霍金经过一系列的计算推理证明,黑洞总是热得像火一样。尽管并未得到观察证实,但他的理论让人信服。黑洞并不总是往里吸东西,也会不停往外放热。这些的热涉及量子力学、广义相对论和热力学。仍未解开。

7. 我们

最后一章是在探讨物理和哲学,就不总结了。

Message Passing Leads to Better Scalability in Parallel Systems

程序员在他们还是个计算机小白时就被灌输并发的概念——尤其是并行(并发的一种子集)——相当有难度的东西,只能寄希望于极少数优秀的人把它们搞明白,但很可能最后他们也是一脸懵逼。有关多线程、信号量、监视器,以及如何并发访问变量的线程安全等问题,常常是人们关注的重点。

说真的,这里面存在很多难题,而且想要解决它们都非常不容易。但这些问题的根源又是什么?共享内存。人们不断遇到的并发问题几乎都和共享内存的使用相关:竞争机制、死锁、活锁,等等。解决这些的答案也很明显:要么放弃并发,要么避开共享内存。

放弃并发?呵呵,别闹了。几乎每个季度计算机都会有越来越多的核,因此真正利用并行优势也变得越来越重要。我们没必要完全依赖处理器的时钟速度的增长,来改善应用程序的性能。只需要充分发挥并行,应用程序性能就会改善。显然,不提高性能可能是个选项,但用户不答应!

所以,我们只能避开共享内存咯?嗯哼。

作为线程+共享内存的替代方案,可以使用进程和消息传递来作为我们的编程模型。这里所说的“进程”不一定是操作系统下的进程概念,只要是一段受保护且独立执行的代码即可。正如Relang(和它之前的occam)之类的编程语言所展示的那样:进程,是在并发和并行系统编程中非常成功的机制。此类系统没有共享内存的同步压力,而多线程系统却有。此外,还有一个正式的模型可应用于此类系统的引擎部分——通信顺序进程(CSP)。

作为一种计算方法,我们会进一步介绍数据流系统。在数据流系统中,没有明确编程的控制流。而是通过一张操作图表,根据数据路径进行连接,将数据馈送到系统中。由系统数据的就绪情况进行评估控制,就杜绝了同步问题。

说那么多,像C、C++、Java、Python和Groovy这样系统级的编程语言,都作为开发共享内存和多线程系统的语言提供给开发者。那该怎么办?答案是使用(如果不存在,就创建)——提供了进程模型和消息传递的库和框架,从而避免内存的使用。

总之,不采用共享内存进行编程,而要用消息传递来取代,在现如今主流的计算机硬件的系统开发中,这或许是充分利用并行性的最好方式。也许这听起来很奇怪:尽管进程之前是用线程作为并发单元,但未来似乎将使用线程来实现进程。

损失厌恶 - 从因果论到目的论

先解释下“损失厌恶”:当人们面对同等程序的收益和损失时,主观上会觉得损失更令人难以接受。举个例子:

一个人买彩票,如果这一期没中500万,其实也无所谓,也不会觉得有什么损失,对吧?但是假如他这一期中了500万,满心欢喜零钱回家,钱还没捂热乎呢,夜里就被小偷洗劫一空。正常人的反应会很愤怒对吧?但是仔细想想,他过往买了很多期彩票,都没中奖,其结果就像是每一期都被小偷光顾了一样。为什么没中奖的时候他反而不会太难过?

所以,人们在面对一件事情的时候,与其让他选择小概率收益,他更乐意选择大概率不要亏损。这就是损失厌恶。

然而在复杂的生活环境中,收益?亏损?其实我们还有第三个选项——不作为!

尽管“不作为”在道德上遭人唾弃,但这真的是大多数人的选择,然而人们为了心安理得地不作为,会在潜意识里编织各种借口来安慰自己,

  • “比尔盖茨爱读书?呵呵,那是人家有钱有闲”
  • “要是我有个有钱的爹,肯定比王思聪强”
  • “被渣男伤过,无法再爱上别人”
  • “我是内向性格,话很少,也比较宅”
  • “先别管我买不买,iPhone的信号问题解决了吗”
  • “中国社会的观念,只会把女性变为生育机器,所以我不结婚”

(以上评论多数源于我刷头条时看到的)

上述这些基本可以概括为佛洛依德式的“原因论”,就是先给出一个无法改变的原因或是过往,然后推导出一个看似合理的结果:“我现在这样,是一种无奈的选择”。

如果你用阿德勒式的“目的论”来看待这些观点就会发现其中的奥妙。

任何一种牵强的理由背后其实都隐藏着“懦弱和懒”的目的,这个目的是由人们的损失厌恶心理引起的,为了达成这个目的,经过潜意识的包装,甚至可以骗过自己。比如:

  • 懒得读书,所以去炮制各种忙/没时间的借口。
  • 自己主观不努力,就从客观找原因,否定他人成就。
  • 不愿再次承受爱情带来的痛苦,所以主动不肯放下过往的情感。
  • 不愿花精力交际,就拿内向来当挡箭牌,觉得话少和宅都是理所当然。
  • 自己不想买或卖不起,就把这一切归功于苹果的技术不行。
  • 通过放大婚姻和生孩子所带来的伤害,来掩饰自己对婚姻的自私。

刚看到这种“目的论”观点的时候,可能会难以接受,先别急着较真儿,我们就从客观上来看看这些评论——发表这些观点的人,就事情本身而言,他们大概率是——做了?还是没做?

答案显而易见对吧,这就是最开始所说的,人们因为损失厌恶,就在收益和亏损之间选择第三项——不作为。

很多事情都有风险,都有好和坏两面。这是客观存在的,也是既定事实。然而对事实的解读却是非常主观的,就像现在的新闻媒体——焦点不同,反映的“事实”就不同。对于个人而言,你总把目光集中在事情消极的那一面,就达成了自己不想作为的目的。(最后举个例子,我们都知道跑步可以减肥,但你却选择听“跑步伤膝盖”这种言论)

阿德勒心理学想要告诉我们的第一个重点就是:人是可以改变的,要不要改变,取决于你对客观事实的主观解读。但,勇敢面对你做或不做为的选择,为其结果负责到底,而不是编造一堆理由,把锅甩给别人。

课题分离 - 人之初,性本“善”

我们的焦虑源自哪里?——社交!

没办法,人是社会型动物,几乎没人可以脱离社会而生存。千万别觉得长期宅在家里就是脱离社会了,你家的水电Wi-Fi和食物可都是社会供给的。

社会让生存变得简单,却让生活变得一点也不简单。

生活的忧愁大多为:活在别人的期待中、对对方的厌烦、朋友同事间无形的攀比、讨厌现在的自己、情感困扰、维护各种亲朋好友的关系…诸如此类吧。这些忧愁基本都源自我与他人的关系。小到朋友/夫妻,到大的国家/民族都属于社交的范畴,它能定义我们与社会某个圈层的关系,我们也能从这个圈层中获得归属感。但人一旦踏入某个圈层,荣辱感也伴随而来,忧愁也就越来越多。再谈这个话题时先说另一个字——善。

善,在希腊语中并不是道德层面善良的意思,而仅仅是“有好处”的意思。而我倒是觉得“人之初,性本善”的善,与其说是善良,不如说是“利己”更为贴切。人的任命之初,其实无所谓善恶,而是跟随本性,去做那些对自己有好处的事情。随着生命的成长,为了融入社会,才逐渐学会了“利他”。

那么阿德勒心理学的第二个重点就是:人要学会“自私”地处理人际关系,你是你,我是我,彼此保持距离,互不干涉。考虑自己该怎么活才是重要的,因为你永远无法让所有人喜欢,你要拥有被讨厌的勇气。

这里的“自私”并不单纯是以自我为中心的意思。简单来说,夫妻是一种社会关系,丈夫爱老婆是丈夫的课题,老婆爱不爱丈夫是老婆的课题,两者彼此独立。然而生活中却经常看到,一方爱,而另一方不爱,导致爱的那方歇斯底里:“我那么爱你,你怎么可以不爱我?”

换而言之,建立了社会关系后,因为混淆了自己和对方的课题,然后发现对方并没有按照自己的意愿去完成课题,从而引发的一系列痛苦。(我觉得这才是真自私)

生活中类似的例子也有很多:

  • 这个领导让我不爽,作为报复,我从此混日子,拖团队后腿
  • 我要给孩子最好的教育,让他将来成为XXX
  • 我是对的,所以你要听我的!
  • 怎么办?说了那样的话,他会不会从此讨厌我?

以上都是混淆了自己和别人的课题,你一心一意为孩子付出,但同时又把自己的主观意愿“让他将来成为XXX”强加给了孩子,孩子以后要成为什么是他自己的人生课题;再如,一个人若要讨厌你,你再怎么言行得体,他还是会讨厌你;总之,一切的一切——“雨女无瓜😄”。

书中的谚语很形象:“把马带到水边,但不能强迫其喝水”。

贡献感 - 有共同体才有幸福

关于课题分离,有的人能理解,有的人会觉得荒谬,还有的人会选择性解读为“我就是我不一样的烟火”这种自私霸道的论调。

这都无所谓,因为阿德勒给出了第三个重点:人生的意义是自己决定的,生活的本质是追求幸福,这就需要我们建立起共同体,并为此持续付出着。

这一点我想用自己的生活态度来解释。面对自己的家庭我总会想——我可是一家之主啊!

我这么想是不是会给人一种大男子主义,道德政治都不正确的感觉?然而并没有,我的这种想法,就是我对家的态度。因为我始终觉得,身为一个爷们儿,我是这个家的首要责任人,这个家以及家人需要我保驾护航,作为这个家的顶梁柱,我要持续地支撑这个家的经济、安稳、进步、价值观等。每当我的家人有困惑的时候,他们更愿意向我寻求帮助,毕竟,我可是一家之主啊。

实话实说,当我携手妻子从小蜗居一步步把住房变大变美,有了孩子,有了更好的生活条件,当每天都能看到小希望,看到家人的快乐时,我内心是幸福的,或许这种幸福也源于一个男人的成就感吧。

同理,我曾和我老婆说过:“你可以选择出去挣钱,也可以选择做家庭主妇,我们可以角色互换,我都尊重你。但是你不能出去挣不到钱(事业上不努力),又以此为借口不去照顾家庭。”一个家里,所有成员都是一体的,你的每一次轻松都意味着对方的付出,你的每一次付出都意味着对方的轻松。

不要总想着付出就意味着自己吃亏了,共同体里有一个很重要的观点是“信赖伙伴”,既然组成了共同体,就应该信赖你的伙伴。这不是什么心灵鸡汤,就好比在中国这个大共同体里,你每天睡觉的时候难道还会担心边防战士有没有好好站岗放哨?这也太可笑了吧!

用目的论看清自己,用课题分离找到适合自己的伙伴,建立共同体,赋予人生意义,然后幸福积极地活着。

但我们总能看到这样的情况:一个女生,打开美颜滤镜瘦脸大眼,对自己的颜值抱有不切实际的幻想,爱上一个大帅哥,并觉得自己的付出驾驭得了他,然后每天查岗管制,最后分手了,她却感慨:“我被渣男伤了,再也无法相信爱情了”😂(我纯属开玩笑)

至此,是我读岸见一郎这本《被讨厌的勇气》后的个人感悟。书的篇幅不算长,但我觉得非常值得一读,倒不是觉得它的内容有多么颠覆,其实里面的很多观点我之前就已经想明白了,只是说它能清晰地把我的一些思想给表达了出来。总之,选个安静的午后,泡杯清茶,舒舒服服地品一下这本书吧。

Make the Invisible More Visible

很多不可见的部分多应作为坚持软件原则被盛赞。我们有很多专业术语来隐喻比可见性,这里说两个——机器透明度和信息隐藏。用道格拉斯·亚当斯(Douglas Adams)的话说,软件及其开发过程几乎是不可见的:

  • 源码不是天然存在的,没有天然的行为,也不可能遵循物理规律。如果你通过编辑器打开它,它就可见,关了后就不可见。想象一下,就像一棵树花了很长时间才倒下,以至于你根本没听见,甚至怀疑它是否存在过。
  • 一个运行着的APP有样子也有行为,但却不会展示其构建的源码。google主页很简单,但它的背后真的很复杂。
  • 如果你完成了90%的工作,却在无限地调试最后10%,其实相当于你并没有做完那90%,同意吗?修复bug并没有在推进项目。调试很浪费时间,没必要花过多时间。最好是让浪费变得可见,你才能看到问题出在哪里,然后开始考虑创建它们的必要性。
  • 如果你的项目已步入正轨,但一周后又发现它需要推迟六个月,那么你的问题来了——最大的问题并不是项目被推迟六个月,而是有某种神秘而强大的力量,之前就对你隐瞒了项目要被拖六个月的事实!进度可见性的缺失,等同于进度的缺失。

不可见会演变成危机。当你有一些具体的事物联想时,你会想得更清楚。当你看得见事物及其不断变化的过程时,你能更好地管理它们。

  • 编写单元测试可以提供代码单元测试难易度的凭证。这有助于暴露代码层面所展示出的开发质量的优势(或缺陷),例如高内聚低耦合。
  • 运行单元测试可以提供代码行为的相关凭证。这有助于暴露应用程序在运行时展现的优势(或缺陷),例如可靠性和稳定性。
  • 使用公告栏或卡片可以是进度变得可见和具体。任务能够以未开始、执行中、已完成被看到,不必引入隐晦的项目管理工具,不必为了虚构一份报告去盯着程序员要。
  • 增量开发增加了开发结果的曝光度,从而提高了开发进度(或滞后进度)的可见性。完成可发布的软件就能暴露事实,光靠估算是不可取的。

大量定期可见的凭证是软件开发的最好方式。可见性给予你底气确信——这个进展是真的,而非骗人的,是经得起推敲的,而非瞎编的,是可复现的,不是歪打正着的。

本文源码:https://github.com/Philon/rpi-drivers/tree/master/05-pwm_musicbox

上一篇用LED呼吸灯的方式,基本介绍了PWM原理以及在树莓派上的驱动开发,但总感觉意犹未尽,所以再写一篇PWM的应用场景——PWM+蜂鸣器,实现一个简易的音乐盒。

说明一下: 本篇纯粹是“玩”,并不涉及任何新的知识点,如果不感兴趣可以掠过。

先来看看我实现的效果:《保卫黄河》、《灌篮高手》、《欢乐斗地主》。这些谱子全是我从网上扒下来的,并根据蜂鸣器的效果修改过,自己不是专业搞音乐的,所以难免会有错误的地方。(反正我耳朵里听着没问题就行😁)

1
2
3
4
5
philon@rpi:~ $ cd modules/
philon@rpi:~/modules $ sudo insmod musicbox.ko
philon@rpi:~/modules $ ./player_test music/01-保卫黄河 # 🎵
philon@rpi:~/modules $ ./player_test music/04-灌篮高手主题曲 # 🎵
philon@rpi:~/modules $ ./player_test music/05-欢乐斗地主 # 🎵

再来看看我是怎么实现的:

  1. 实现PWM蜂鸣器驱动musicbox:通过write音符给设备节点,播放不同的声音;通过ioctl控制节拍
  2. 实现应用层player_test,负责读取歌曲的乐谱
  3. 编写乐谱,其实就是文本简谱,类似下面这首
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C 3/4

# ~前奏~
(5` (4`) 3` 2` 1` 7 1` 0 3 (2) 3 5 (6 5 6 1`) 5 0)
(6` (5`) 4` 3` 2` 1` 7 6 5 6 1` 2` 5 6 2` 3` 5 6 3` 4` 5 6 4` 5`)

# 风在吼,马在叫,黄河在咆哮,黄河在咆哮
1` (1` 3) 5- 1` (1` 3) 5- (3) 3 (5) 1` 1` (6) 6 (4) 2` 2`

# 河西山岗万丈高,河东河北高粱熟了
(5 (6) 5 4) (3 2 3 0) (5 (6) 5 4) (3 2 3 1)

# 万山丛中,抗日英雄真不少
5 (6) 1` 3 (5 (3`) 2` 1`) 5 6 3-

# 青纱帐里,游击健儿真不少
5 (6) 1` 3 (5 (3`) 2` 1`) 5 6 1`-

# 端起了土枪洋枪,挥动着大刀长矛
(5 (3 5) 6 5 1` 1`) 0 (5 (3 5) 6 5 2` 2`) 0

# 保卫家乡,保卫黄河,保卫华北,保卫全中国
(5 (6) 1` 1`) 0 (5 (6) 2` 2`) 0 (5 (6) 3` 3`) (5 (6) 3` 2` 1`----)

好,具体实现且听我慢慢道来~

PWM蜂鸣器驱动

有关蜂鸣器硬件原理、有源、无源这里不展开讨论。总之本文采用的是树莓派上的PWM0+一个无源蜂鸣器。接线如下图所示:

PWM蜂鸣器树莓派接线图
PWM蜂鸣器原理图

根据上一篇《PWM呼吸灯》的学习,基本知道PWM对脉冲的控制主要有占空比脉冲周期两部分。用来控制LED的时,占空比可以调节灯光的强弱,在脉冲周期似乎没什么乱用。

对于蜂鸣器声用作声乐,有三个基本要素:音调、节拍、音量大小。

  • 音调:由震动频率决定,对应PWM的脉冲周期
  • 音量:同样的频率,PWM占空比越高,声音越大
  • 节拍,声音的持续时长,和PWM毛关系都没有,做个定时开关即可

综上,其实在蜂鸣器驱动musicbox里重点实现两个接口:

  • write: 解析用户层写入的字符串,例如音调do的高中低音分别为’1`‘和’1‘和’1.‘,然后换算出对应的频率即可。
  • ioctl: 解析用户层发来的指令,有节拍、音调、音量等控制。

不同音调的蜂鸣器频率

注意:此部分涉及的乐理知识我不是很懂,基本是从网上抄来的,但我发现F和B调的发音不是很准,估计频率不对。

下表分别是Do Re Mi Fa So La Ti对应的蜂鸣器震动频率。

wait-!7个音符,怎么会干出13种频率呢?

因为其中涵盖了A-G不同曲调,一首曲子可以由多个调子来演奏,比如我们经常听到的C小调,D大调之类的。其中的乐理只是更为复杂,这里只需要记住:

C调的Do为基准,其他调子做相应偏移。例如E调的Do相当于C调的Mi,而A调的Do相当于C调的La。

音域 1 2 3 4 5 6 C7 D7 E7 F7 G7 A7 B7
低音 131 147 165 175 196 221 248 278 312 330 371 416 467
中音 262 294 330 350 393 441 495 556 624 661 742 883 935
高音 525 589 661 700 786 882 900 1112 1248 1322 1484 1665 1869

如何计算PWM的周期

有了不同音符的震动频率,也就得到了PWM的脉冲周期。举个例子,50Hz相当于1秒钟震动50次,那PWM的脉冲周期就应该为1s/50=0.02秒。因此周期的计算公式为:

period = 1s / freq

其中的freq就是音符表中的频率,而1s可以由Linux中的HZ变量表示。

如何计算PWM占空比

有了脉冲周期,才能计算占空比。一个周期内高电平所占时间越大,输出声音也就越大。所以我们可以通过百分比来决定占空比大小。

假设现在要输出高音3`,它对应的频率为661,并根据前面的公式求得脉冲周期为12345,而音量为75%,那占空比应该为12345*75/100 = 9528。因此占空比的计算公式为:

duty = period * volume / 100

如何计算节拍

所谓节拍,如2/4拍,表示以4分音符为一拍,每小节有两拍。

但在程序里,节拍即每个音符输出的时长,这一点我并没有在驱动层实现,但做的做法非常简单。并没有引入“小节”和“动次打次”的概念。就是强制一个小节为4秒,如果是2/4拍,就相当于4000/4/2 = 500毫秒,即每个音符默认响0.5秒。如果存在半拍的情况(就是音符下有画线),那时间再减半。

程序中把半拍用圆括号()表示,遇到左括号就减半时间,遇到右括号就加倍时间,就是那么粗暴。

驱动程序实现

当加载以下驱动后,可以通过命令行echo 1 > /dev/musicbox来测试是否会响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include <linux/module.h>
#include <linux/pwm.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>
#include <linux/timer.h>
#include <linux/wait.h>

#define MUSICBOX_MAX_VOLUME 100 // 最大音量100%
#define ONE_SECOND 1000000000 // 以纳秒为单位的一秒

// 各音域音符对应震动频率
static const int tones[][14] = {
// C D E F G A B
// 1. 2. 3. 4. 5. 6. 7.
{0, 131, 147, 165, 175, 196, 221, 248, 278, 312, 330, 371, 416, 476},
// 1 2 3 4 5 6 7
{0, 262, 294, 330, 350, 393, 441, 496, 556, 624, 661, 742, 833, 935},
// 1' 2' 3' 4' 5' 6' 7'
{0, 525, 589, 661, 700, 786, 882, 990, 1112, 1248, 1322, 1484, 1665, 1869},
};

static struct {
bool playing; // 是否正在播放
wait_queue_head_t wwait; // 写等待
struct pwm_device* buzzer; // 蜂鸣器
struct timer_list timer; // 定时器
char volume; // 音量 0-100
char tonality; // 音调 A-G
char beat; // 节拍
char key; // 音调
} musicbox;

static void music_stop(struct timer_list* timer) {
pwm_disable(musicbox.buzzer);
musicbox.playing = false;
wake_up(&musicbox.wwait);
}

static ssize_t musicbox_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) {
// 获取音符
int note = (buf[0] - '0') + musicbox.key;
// 获取音域
int pitch = (buf[1] == '`') ? 2 : (buf[1] == '.' ? 0 : 1);
// 根据频率计算脉冲周期
int tone = ONE_SECOND / tones[pitch][note];
// 根据脉冲周期计算音量
int volume = tone * musicbox.volume / 100;
// 当音符后跟着'-'就延长一倍时间
int delay = HZ / musicbox.beat * (len - (pitch != 1));

// 写阻塞,一次只能播放一个音符
if (musicbox.playing) {
if (filp->f_flags & O_NONBLOCK) {
return -EAGAIN;
} else {
DECLARE_WAITQUEUE(wq, current);
add_wait_queue(&musicbox.wwait, &wq);
wait_event(musicbox.wwait, !musicbox.playing);
remove_wait_queue(&musicbox.wwait, &wq);
}
}

pwm_config(musicbox.buzzer, volume, tone);
if (buf[0] > '0') {
pwm_enable(musicbox.buzzer);
}
mod_timer(&musicbox.timer, jiffies + delay);
musicbox.playing = true;

return len;
}

// 音量、音调、节拍控制
static long musicbox_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case MUSICBOX_SET_VOLUMN:
if (arg >= 0 && arg <= MUSICBOX_MAX_VOLUME) {
musicbox.volume = arg;
} else {
return -EINVAL;
}
break;
case MUSICBOX_GET_VOLUMN:
return musicbox.volume;
case MUSICBOX_SET_BEAT:
if (arg > 0 && arg <= 1000) {
musicbox.beat = 1000 / arg;
} else {
return -EINVAL;
}
break;
case MUSICBOX_GET_BEAT:
return 1000 / musicbox.beat;
case MUSICBOX_SET_KEY:
if (arg < 'A' || arg > 'G') {
return -EINVAL;
}
musicbox.key = arg >= 'C' ? (arg - 'C') : (arg - 'A' + 5);
break;
case MUSICBOX_GET_KEY:
return musicbox.key;
default:
printk("error cmd = %d\n", cmd);
return -EFAULT;
}
return 0;
}

// 以下是设备驱动注册/注销相关
static const struct file_operations fops = {
.owner = THIS_MODULE,
.write = musicbox_write,
.unlocked_ioctl = musicbox_ioctl,
};

static struct miscdevice mdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "musicbox",
.fops = &fops,
.nodename = "musicbox",
.mode = S_IRWXUGO,
};

int __init musicbox_init(void) {
musicbox.buzzer = pwm_request(0, "Buzzer");
if (IS_ERR_OR_NULL(musicbox.buzzer)) {
printk(KERN_ERR "failed to request pwm0\n");
return PTR_ERR(musicbox.buzzer);
}

musicbox.volume = 50;
musicbox.tonality = 'A';
musicbox.key = 0;
musicbox.beat = 4;

init_waitqueue_head(&musicbox.wwait);
timer_setup(&musicbox.timer, music_stop, 0);
add_timer(&musicbox.timer);

misc_register(&mdev);

return 0;
}
module_init(musicbox_init);

void __exit musicbox_exit(void) {
misc_deregister(&mdev);
del_timer(&musicbox.timer);
pwm_disable(musicbox.buzzer);
pwm_free(musicbox.buzzer);
}
module_exit(musicbox_exit);

应用层加载乐谱

应用层player_test程序的业务逻辑就简单得多了:

  1. 加载指定的乐谱文件
  2. 配置音乐盒的节拍、音调
  3. 按行读取文件内容(跳过注释行和空行)
  4. 提取每一行的音符、括号
  5. 将音符、节拍写入驱动
  6. 重复第3-5步,直至文件末尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>

#include "musicbox.h"

#define MUSIC_BOX_FILE "/dev/musicbox"

int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: ./player <musicfile>");
return -1;
}

int fd = open(MUSIC_BOX_FILE, O_RDWR);
if (fd < 0) {
perror("open musicbox");
exit(0);
}

FILE* music = fopen(argv[1], "r");
if (music == NULL) {
perror("open music");
exit(0);
}

// 初始化音乐盒
char line[128] = {'\0'};
if (fgets(line, sizeof(line), music) == NULL) {
perror("read music");
exit(0);
}

// 4秒为一节,计算每拍的长度,如2/4拍时,每拍长度为500ms
int beat = 4000 / (line[4]-'0') / (line[2]-'0');
if (ioctl(fd, MUSICBOX_SET_BEAT, beat) < 0
|| ioctl(fd, MUSICBOX_SET_VOLUMN, 90) < 0
|| ioctl(fd, MUSICBOX_SET_KEY, line[0]) < 0) {
perror("ioctl");
exit(0);
}

// 按行加载乐谱文件
while (fgets(line, sizeof(line), music)) {
printf("%s", line);
if (line[0] == '#' || line[0] == '\0') {
continue;
}

char* p = line;
while (*p) {
if (*p == '(') {
ioctl(fd, MUSICBOX_SET_BEAT, ioctl(fd, MUSICBOX_GET_BEAT) / 2);
} else if (*p == ')') {
ioctl(fd, MUSICBOX_SET_BEAT, ioctl(fd, MUSICBOX_GET_BEAT) * 2);
} else if (*p >= '0' && *p <= '7') {
char* q = p+1;
while (*q == '`') q++;
while (*q == '.') q++;
while (*q == '-') q++;
write(fd, p, q-p);
}
p++;
}
}

close(fd);
fclose(music);
return 0;
}

小结

由于本章没有新的知识点,就不做知识总结了,说说感受。

当蜂鸣器按照我的预期演奏音乐是还是挺开心的,仿佛一下子把我拉回了大学的那个暑假,一个人默默在宿舍鼓弄51单片机的日子。时光荏苒,尽管做的是同一件事,但我现在的软件架构、编程基础不可同日而语。或许我重拾底层技术的同时,也重拾了当年学习的热情吧😊。

《解忧杂货铺》讲述的是三个小偷在一名富婆家中盗窃+抢劫后,逃到一间破败已久的杂货铺中躲避。而这家店主生前喜欢通过匿名信件来往的方式,为孩子及年轻人排忧解难,一开始都是些玩笑,后来却真的有人咨询起人生烦恼。久而久之,店铺门前的牛奶箱就变成了信箱。

店主曾建议一位怀有身孕的情妇留住孩子,导致这名女子成了单身妈妈而无法在社会立足,最终疑似自杀。尽管事后证明女子很爱自己的孩子,所谓自杀纯属一场意外,但店主深刻认识到自己的建议会影响前来咨询的人们的命运轨迹,并十分诡异地断言自己33年后的忌日,店铺的咨询服务窗口会复活,希望那些曾今被他建议的年轻人,再回一封信如实反馈之后的生活命运,他想要知道自己的建议是否有积极的作用,只求心安。

碰巧的是,三个小偷躲进来的当晚正好在店主33年后的忌日,更离奇的是他们收到了40年前的咨询信。活在社会底层的他们第一次发觉身上居然还有一些帮助他人的价值。就这样,在这连接着过去未来的杂货店里,他们跨越时空去帮助曾今的年轻人。

  • 一个是运动员,她即将参加明年奥运会选拔,可男友身患癌症,不知该继续训练还是陪伴男友最后的生命时光。
  • 一个是穷酸的音乐人,他追逐音乐梦多年无果,不知该继续追逐还是回家子承父业。
  • 一个是刚毕业的女大学生,小时候被养父母从孤儿院认领并抚养至今,她希望做陪酒小姐快速赚钱来给父母最好的生活。却很担心被家里知道,她很困惑。

三个小偷利用“未来的历史”向过去的人给出最正确的选择,然而令人诧异的是,其实每个人心中早已有了自己的答案或倾向选择,他们的来信咨询无非是为自己下定决心。不论杂货店给他们什么建议,他们最开始都是反驳,然后跟着感觉走,最后又感叹杂货店的精准预言,并表示感谢。

不论如何,只有那个女大学生最终听从了他们的建议,在最恰当的时机买房买股票,运作事业,从此飞黄腾达。不过最讽刺的是,三个小偷昨晚入室抢劫的女主人,正是他们在杂货店里帮助过的女大学生。

简单点评

不得不说,东野圭吾的写作手法还是挺新颖的,明明是部暖心治愈的小说,却偏偏用类似悬疑推理的框架来写,刚开始读的时候我还以为是部惊悚小说😂。不过随着谜团的一点点挖开,心中的疑问得到解答,才觉得这样的叙事逻辑还挺不错呢。

书中有很多的人物关系,但是要理清爽却不难,因为他们都和那个小镇、杂货店、孤儿院或多或少有些关系。一开始我会觉得“怎么可能那么巧”,其实后来想想,就把它当作专门讲述这个小镇以及孤儿院里的神话传说就好,即便所有的人物都和这扯上关系也很正常嘛。

此外,可能是我读的书一般都是各种心理学、科普、哲学之类的,总觉得每本书背后都会蕴含某个重大的理论或哲理。当然,这部小说出了暖心、治愈之外,好像也没什么,简简单单,东野圭吾的一部小说,挺好看的。

简单感悟

我们时常感叹命运的轮回,悔不当初。可曾想过冥冥之中,我们早已做好决定?《解忧杂货店》之所以好看,正是给了读者一个意淫的空间——触摸到过去,在自己力所能及的范围去改变过去,哪怕改变的那个人不是我。

这部小说没有一个明确的主角,就暂且把三个小偷当作主角吧。他们是活在社会最底层的人群。文化低/见识少/头脑简单,不知还有多少贬义的标签可以往他们身上贴。但重要的一点是,越是这样的人越会想一个问题:“要是当年我XXX,现在肯定XXX”。没错,这也是一种意淫,也是庸人自扰的源头。

忧,是对当下生活的无力感,也是对过去抉择的痛楚,那种痛楚并非简单的悔恨,而是死不认输的倔强,想要把现状变好以证明自己当初的抉择是对的,然而面对当下又很无奈,呵呵!

其实,看看过去的来信人,在他们把信服递进卷帘门缝之前,内心早已有了抉择。运动员其实更希望去赛场上拼搏,音乐人更希望继续追逐梦想,女大学生更是从一开始就明确要继续做陪酒小姐。那么他们何必多此一举写信咨询呢?东野圭吾早已在书中给出了理由,即音乐家父亲的那句话:

到了那个时候,你有很多理由替自己开脱。‘因为我爸病倒了,没办法只能继承了’,‘都是为了这个家做出的牺牲’,总之什么责任也不想负,全是别人的错。

这就好比现实生活中的我们,在下任何重大决定之前,总会先听听别人的建议,然而很多时候真的只是听听而已。我们在做决定前内心早已有答案,只不过需要先为自己可能的失败找好台阶。就好像在比赛前我们总会说:“哎,今天状态不好,别和我抢最后一名哈”,然而枪声一枪,跑得比谁都狠。

再有,要是心中还有“要是当年选择XXX就好了”的念头时,不妨换个思路想想,你已经穿越到未来20年后,可以给现在的你写一封信,你希望给自己什么样的建议呢?心中有些答案了么?还是说你会像三个小偷一样,只希望得到明确的彩票号码或股票代码或投资时机。😜

我想象中的结局

读到最后一章,晴美说自己要去做陪酒小姐,而敦也说自己的妈妈也是小姐,在那种环境下的女人会因纸醉金迷而丧失人的基本尊严。他也正是从小目睹到这一切,才在心中极力劝阻晴美不要选择这条路,终究是不归路。

恰好最后一章的标题是“来自天上的祈祷”,我在想会不会是这样的结局:

晴美其实就是敦也的妈妈,虽然不知道生父是谁,但妈妈始终想要给孩子最好的生活,因此更卖力的工作,但在这种环境呆久了,除了讨好男人什么也不会,结果不仅得不到儿子的理解,还一而再再而三被男人玩弄情感。不论生活还是心灵,都无法给儿子有利的成长环境。她近乎绝望,计划自杀并让孩子进入孤儿院。她十分纠结,所以提笔向解忧杂货店咨询。信,被未来的敦也收到了,由于对小姐行业的排斥心理,他自然在回信上一顿痛骂。但三番五次交流后,敦也似乎意识到咨询人可能是母亲年轻时候。敦也立刻回信劝阻母亲不要放弃生活,可惜母亲内心早有答案了,最后一封信投了进来:“浪矢先生,很感激你的开导。但我终究不知该如何面对孩子,他已经开始恨我了,我不希望因为自己的职业给孩子的未来造成困扰,我已经毁了自己的生活,不能再毁掉他的。在这里还要麻烦您一件事,如果你在镇上孤儿院里见到一个叫敦也的小男孩,恳请帮我转达:‘妈妈会在天上为你祈祷,妈妈永远爱着你’,拜托了!”

以上,纯属我意淫😁

东野圭吾给出的结局更微妙,晴美在飞黄腾达后只想在33年忌日那天回感谢信,碰巧遇上三个小偷,信和财务被连带洗劫一空。晴美很失落,但三个小偷最后很无语,因为他们确实收到了那封感谢信,尽管是他们自己抢来的。

Make Interfaces Easy to Use Correctly and Hard to Use Incorrectly

软件开发中最常见的一项任务就是接口规范。接口可以是顶层抽象(用户接口),可以是底层抽象(函数接口),也可以在两者之间(类接口、库接口等等)。不论你是否向最终用户说明过他们该如何与某个系统交互,或是向开发者解释如何调用API,或者给类声明私有函数,接口设计都会是你工作中非常重要的一部分。如果你做得好,接口就会被乐于使用,增加生产率。如果你做得不好,接口就会是抱怨和错误的源泉。

好的接口应该是这样的:

易于正确使用

人们总能正确地使用优秀的接口设计,因为路径阻力最小。对于图形界面,他们总是可以正确地点击到图标、按钮、或者菜单入口,因为它太明显也太容易了。对于API,开发者总能正确地传递对的参数和对的值,因为它看起来理所当然。面对那些易于被正确使用的接口——直接用就是了。

难以用错

好的接口可以预测人们可能犯的错,并让它难以朝那方面想,几乎不可能去提交这种错误。一个图形界面可能会禁用或者益处那些在当前上下文中没有意义的命令。例如,API可以通过“允许传递任意顺序的参数”来消除传递参数时的顺序问题。

设计易于正确使用的接口的好办法是在它们出现之前就开始使用它们。模拟一个图形界面——可以是一个白板或者桌上的索引卡片——在你创建任何底层代码之前就先开始用它们。再比如,在函数被声明之前就应该先去写如何调用它的代码。浏览常见的用例,并指定你期望该接口会产生的行为。你想点击什么?你想通过什么?易于使用的接口看起来总那么自然,因为它能让你做你想做的。如果你从用户视角去开发它们,你更有可能做出像样的接口。(这个观点也是测试优先编程(test-first programming)的优势之一)

要让接口难以用错需要做两件事。首先,你必须预测用户可能犯的错,并找到阻止它们的方法。第二,你必须留意在早起发布期间接口是如何被滥用的,然后修改接口——是的,修改接口!——以阻止那些错误。禁止错误使用的最佳途径是让它们不可能这么用。如果用户坚持撤销一个不可能的撤销动作,就试着让这个动作变得可以撤销。如果它们坚持传入一个错误的值到API里,尽力修改API以让用户传递想要的值。

综上,记住,一个接口的存在是为了方便它们用户,而非它们的实现者。

坦白说,这部小说没什么跌宕起伏,没有太多吸引我的剧情,一切都那么平淡,就像书名一样——月亮?还是六便士?那么直白,那么简单,那么毫无深度的哲学。但不知为何,我总是魂牵梦绕地去想象:“斯特里克兰德的画到底是什么样子?”不得不说,我几乎是读到本书末尾,才由衷地感叹毛姆在描写人性时,尺度拿捏之精准,在下佩服!

以上仅是我在读此书的一点小感触,如果没品味过这本书,可能会觉得我表达得有些莫名其妙。算了,还是来点谈不上剧透的剧透吧。

《月亮与六便士》讲述了一位40来岁的中年男人离家出走转行做画家直到死亡的故事。他叫查尔斯·斯特里克兰德,在英国伦敦的证券交易所的交易员,生活美满家庭幸福,由于突然发现自己真正热爱的是画画,他便毫无征兆地出走,再也没回来。他去了巴黎,妻子及亲戚对他这种“始乱终弃”的行为咬牙切齿,他本人也由于性格的怪异,对世俗的冷漠,毫不在意与人社交的个性,使得他生活极为窘迫。但是这都无所谓,他只在乎画画,他只要精神追求。终于,把巴黎的朋友搅得天翻地覆之后,他流浪到了一个叫塔希提的热带小岛。那里与世隔绝,没什么是非恩怨,他在那里娶妻生子,静心创作,直到病故。死后,和很多画家一样无聊的命运,人们开始发现他的天才作品,他成了棺材里的明星,连他最初的妻子和亲戚都站出来像民众讲述他生前的事迹。

故事到此结束,这便是剧情梗概,整部小说不过是对这无聊的剧情扩充一个又一个的小插曲,一段又一段的闲言碎语,一堆又一堆的时间地点人物,仅此而已。很无聊对吧,但小说的尾声才是最精妙的地方。

或许很多读者都认为本书的主题就是颂赞一个追求精神充盈的伟大人物,唤醒读者要月亮而非六便士的潜层渴望。但我不以为然,我从书里读到的是——人性与文明。

爱与自私

婚姻,不论你和谁结婚,你都不可避免的要处理婚姻中的“裙带关系”。斯特里克兰德总共有三段所谓的婚恋史(他并没有和原配离婚)。

一段是英国伦敦的原配,一段是法国巴黎的朋友妻,一段是荒岛上的土著。

英国伦敦:丈夫的无故出走,让妻子伤心欲绝。“是我做得不够好吗?是他另有新欢了吗?是他厌倦我了吗?我该怎么和家里人交代?我今后要如何生活下去?孩子怎么办?为了这个家,忍忍吧,只要他肯回来,我全当什么都没发生过!”这是妻子遭遇晴天霹雳的反应,也能代表很多人的心理,当得知丈夫绝不会回家后,妻子以娘家作为后盾,励志要坚强起来,活得比现在还精彩,要让那个负心汉后悔当初的选择。

法国巴黎:朋友的妻子爱上了查尔斯,她表现出女性对爱情最坚实的渴望,无所谓世俗眼光,不惧背负任何骂名,只要和这名落魄的画家在一起。最终,查尔斯依旧抛弃了她,而她选择了自杀。而表面上的原因也很简单——腻了!

塔希提岛:一个土著黑人女性爱上这个白人画家,它们搬去了一个荒岛,与世隔绝,没有任何多余的社交。用文明世界的眼光来看,他们活得和原始人一样,然而他们从此无忧无虑,唯一的心痛便是夫妻的生离死别。

查尔斯的前两段婚恋,让人觉的他就是个禽兽,但想想艺术家似乎就该有一丝风流和痞气,倒也情有可原。但是却又想不通,最后这个没见过世面的女人,她又凭什么俘获了这个天才画家的心?

小说最后,当查尔斯举世闻名,记者去采访原配妻子,那个满心仇恨的女人,现在也能平静地向人们讲述她丈夫的生前;她的亲戚们,那些当初对他各种咒怨的亲戚,也在得知他后半生的悲惨后,流露出些许同情。理由很简单,画家死后的光环为这个家族带来了荣誉。而那个在法国巴黎自杀的女人,其实和这位英国伦敦的原配妻子一样——在爱这个男人的同时,用尽各种技俩试图在精神上占有这个男人。当然这种技俩的外表通常是爱的名义,不论用何种甜言蜜语,何种温柔体贴,本质上是满足拴住自己男人的私心。

最后这位黑人小姑娘,或许是当地的文化,或许是没见过世面,她并没有选择栓住他。而是真正的陪着他,不打扰,不侵占,不霸道的爱。

六便士,终将是大多数人的选择

从农耕文明开始,人类摒弃了游山玩水的原始生活。定居以后,我们发展出了部落、村庄、国家、社会。与之相伴的还有文化、经济、工业、科技,玲琅满目的新闻。我们的作息从随遇而安变成了每天工作XX小时,我们的意义从纯粹的生理需求变为了社交需求,我们竭尽全力追求理想生活,然后时不时地问自己:“幸福了吗?”

仔细想想,查尔斯为何在荒岛上找到了真正的快乐,而非伦敦,或者艺术气息浓厚的巴黎。因为当他灵魂苏醒的那一刻,他就明白了自己将全力以赴献身绘画创作,这是一种纯粹的精神追求。为此,他才会对饥饿、性冲动、社交、金钱,这等物欲恼火,如果可以,他真的不希望被这些事情分心。他抛妻弃子,冷落朋友,活成一个要饭的,是因为他清楚这是社会,不是艺术。

荒岛是个奇妙的地方,那是人类文明未曾侵犯的净土。你为了追求艺术而蓬头垢面,天马行空,但没人会觉得你很怪。那里没有繁杂的社交活动,那里还是原始社会,人们的行为更多还是被本能驱使,不会渗入过多的杂质。就一个艺术家而言,那里才是灵魂该去的地方。

但,你的灵魂在何方?抬头看看月亮——你能为此抛下文明吗?

《月亮与六便士》可以被理解为《理想还是现实》。当然,我更愿意认为它是《净土还是文明》。因为我很清楚,心心念的理想,不过是忽略了文明对我的影响,我不可能把自己活成原始人。

The Longevity of Interim Solutions

我们为何要创建临时解决方案?

通常,总有一些紧迫的问题需要解决。它可能是开发团队内部的问题,例如用某些工具去填补工具链的空白。它也可能是外部问题,对最终用户可见,例如通过变通办法处理缺失的功能。

在很多系统和团队里,你会发现一些系统会有隔离软件,它们被当作随时会变更的草案,即不符合标准,也未形成代码指南。而且可以肯定,你会听到开发者总在抱怨它们。最初创建这些的原因多种多样,但一个临时方案的成功非常简单:它有用!

临时解决方案,无论如何,它获得了惯性(或者说动能,取决于你的观点)。因为它就在那里,最终有用且被广泛接受,并没什么紧急要做的事情。每当权衡利弊来决定哪种行为价值更大时,就会出现很多排名较高的,适合于整合到系统的临时解决方案。为什么?因为它就在那里,它正常运行,它被接受了。唯一的明显缺点是它不符合约定好的标准和指南——除了一些利基市场,但这并不重要。

所以临时解决方案仍然存在,永远。

如果临时解决方案出了问题呢,它大概率不会出现在产品质量认证的条目里。那怎么办?当然是迅速用一个临时更新来替代这次临时方案啦,这也将收获好评。它将和最初的临时方案一样健壮…只是更新了一个版本而已。

这有什么问题吗?

答案取决于你的项目,以及你个人在产品代码标准中的利益。当一个系统包含了太多的临时解决方案,它的熵或者内部复杂度就会上升,可维护性就会下降。不论如何,这可能是个首先要问的错误问题。请记住我们在探讨的是解决方案,而不是你更喜欢的那个方案——它不大可能是所有人都喜欢的方案——但重做这个方案的动机很弱。

所以当我们发现问题是该做什么呢?

  1. 避免一开始就创建临时方案。
  2. 改变那些影响项目经理决策的因素。
  3. 远离它。

现在让我们进一步来探讨这些选项:

  1. 很多时候,回避并不是一种选项。实际的问题就摆在眼前,而且标准又那么严格。你可能会伤精费神地去尝试改变标准——尽管那是单调乏味的努力——且这种改变通常不会对你手头的问题有任何影响。
  2. 这些因素源自项目文化,那些抗拒改变的本能。如果项目非常小的情况下倒是可能成功——尤其是项目中只有你自己——以及在和高层反映之前就已经清理了混乱。当然,如果项目已经混乱到停滞的地步,(方案)也可能推行成功,不过通常需要一些时间来接受。
  3. 如果前面两种选项都不行,那就自动进入这个选项。

你会创建很多解决方案;它们其中一些是临时的,绝大部分是有用的。克服临时方案的最佳途径是让它们变得多余,提供更为优雅和有用的方案。愿你能平静地接受那些无法改变的事物,而鼓足勇气去改变那些可以改变的,并拥有甄别这些区别的智慧。

本文源码:https://github.com/Philon/rpi-drivers/tree/master/04-pwm_led

如果你对硬件了解不是很深,看了标题可能会一脸懵逼,PWM是什么?别急,在回答这个问题之前,先看看PWM+一个普通的LED灯能实现什么效果:

pwmled.gif

(动图有点大,请稍等…)

从结果来看可能第一反应是这样的,普通的LED灯只能控制开/关,既然PWM可以控制LED的亮和暗,那它应该是一种电压控制器!其实根本不是那么回事,和电压毛关系都没有。它背后的原理恰恰是它最有意思的地方,它的本领可不仅仅是拿来控制个灯的明暗那么low。

PWM基础

PWM——脉冲宽度调制,顾名思义,就是一个脉冲信号输出源,且方波的周期(period)以及高电平持续时间(duty)是可调的。从软件角度来看,主要关注两个地方:

  • period,脉冲周期,就是发送一次1和0交替的完整时间
  • duty,占空比,就是一个脉冲周期内1占了多长时间。

有时候,还需要关注1秒钟发送多少个脉冲信号,即频率,不过对于本文要控制的led灯亮度而言,频率几乎可以忽略。关于PWM原理,如果还是云里雾里,就看一下这个视频介绍,非常简单。

不过先提醒以下,PWM的实际应用非常广,有红外遥控、音频控制、通信等,最常见的要数直流电机控制,以及手机上的背光灯亮度。总之,掌握pwm的原理及应用是灰常有必要滴。

树莓派上的PWM

树莓派上的PWM比较坑,明明很简单的东西我调了两天,网上99%的教程都是各种应用层的脚本或writingPi的函数调用,和Linux内核的边都沾不上,而且其中绝大多数还是相互复制粘贴。所以真正有用的资料寥寥无几,一直卡在pwm_request却获取不到pwm资源,当然最主要原因还是我对技术细节的不理解,好在发现了一个老外的博客:https://jumpnowtek.com/rpi/Using-the-Raspberry-Pi-Hardware-PWM-timers.html

在官方的《BCM2835外围指南》第9章说得很清楚,处理器内部集成了两个独立的PWM控制器,理论上可以达到100MHz的脉冲频率。树莓派扩展接口共有4个GPIO引出PWM,具体为:

PWM通道 GPIO号 物理引脚 复用功能
PWM0 GPIO12 32 Alt Fun 0
PWM1 GPIO13 33 Alt Fun 0
PWM0 GPIO18 12 Alt Fun 5
PWM1 GPIO19 35 Alt Fun 5

第一步,启用pwm(默认情况下未启用)

简而言之,你无法通过Linux内核API获取到PWM资源,因为在树莓派官方的设备树配置(/boot/config.txt)里并没有通知内核要启用pwm。因此第一步自然是让内核支持pwm驱动,使用如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
# vim打开/boot/config.txt
# 在最后一行加入: dtoverlay=pwm
# 保存退出,重启

philon@rpi:~ $ sudo vim /boot/config.txt
philon@rpi:~ $ sudo reboot

# 重启之后,有两种方式确认pwm已启用
philon@rpi:~ $ lsmod | grep pwm
pwm_bcm2835 16384 1 # 方式1: 加载了官方pwm驱动

philon@rpi:~ $ ls /sys/class/pwm/
pwmchip0 # 方式2: sysfs里可以看到pwmchip0目录

第二步,搭建硬件环境

非常简单,LED的正极拉到一个PWM通道,负极随便找一个GND接上。

PWMLED接线图

如图所示,将三色LED的绿灯脚接到GPIO18,也就是PWM0通道,再随便找一个GND接上即可,和第一章的点亮一个LED一样,关键看软件如何操作了。

PWMLED原理图

使用命令行控制PWM

根据之前的硬件接线,LED与树莓派的PWM0通道相连,所以使能pwm0即可点亮led,大体步骤为:

  1. 请求pwm0资源
  2. 设置脉冲周期
  3. 设置占空比
  4. 打开pwm0

命令行控制pwm其实和gpio大同小异,都是通过sysfs这个虚拟文件系统完成的。

1
2
3
4
5
6
7
8
9
10
11
philon@rpi:~ $ cd /sys/class/pwm/pwmchip0/    # 进入pwm资源目录

philon@rpi:~ $ echo 0 > export # 加载pwm0资源
philon@rpi:~ $ echo 10000000 > pwm0/period # 设置脉冲周期为10ms(100Hz)
philon@rpi:~ $ echo 8000000 > pwm0/duty_cycle # 设置占空比为8ms
philon@rpi:~ $ echo 1 > pwm0/enable # 开始输出

# 可以自行调整脉冲周期和占空比,得到不同的亮度
# 如果玩够了,记得释放资源
philon@rpi:~ $ echo 0 > pwm0/enable # 关闭输出
philon@rpi:~ $ echo 0 > unexport # 卸载pwm0资源

经过上面这番犀利操作,只要你够虔诚,就会看见一束绿光闯入你的眼里,心里,脑海里。

Linux驱动控制PWM

在实现pwm调光led之前,需要先交代清楚:

由于本驱动仅仅是调节led的亮度,关于pwm的占空比脉冲周期两个参数,其实只调节占空比就够了。你想一下,脉冲周期长短,无非就是led闪烁频率,只要人眼看着不闪,再快有个毛用。因此本驱动将脉冲周期固定为1ms,即1KHz,而占空比的取值为0~1000us,说白了,就是led从最暗到最亮一共分为一千个档位。

内核为pwm提供了标准的API接口,需要掌握这几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// PWM channel object
struct pwm_device {
const char *label; // name of the PWM device
unsigned long flags; // flags associated with the PWM device
unsigned int hwpwm; // per-chip relative index of the PWM device
unsigned int pwm; // global index of the PWM device
struct pwm_chip *chip; // PWM chip providing this PWM device
void *chip_data; // chip-private data associated with the PWM device
struct pwm_args args; // PWM arguments
struct pwm_state state; // curent PWM channel state
};

/**
* 通过pwm通道号获取pwm通道对象
* @pwm_id 通道号
* @label pwm通道别名
*/
struct pwm_device *pwm_request(int pwm_id, const char *label);

/**
* 释放pwm通道对象
*/
void pwm_free(struct pwm_device *pwm);

/**
* 设置pwm通道的相关参数
* @duty_ns 以纳秒为单位的占空比
* @period_ns 以纳秒为单位的脉冲周期
*/
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)

/**
* 打开pwm通道,开始输出脉冲
*/
int pwm_enable(struct pwm_device *pwm)

/**
* 关闭pwm通道,停止输出脉冲
*/
void pwm_disable(struct pwm_device *pwm)

接着,实现pwmled的驱动吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/********************* pwmled.h *********************/

#define PWMLED_MAX_BRIGHTNESS 1000

typedef enum {
PWMLED_CMD_SET_BRIGHTNESS = 0x1,
PWMLED_CMD_GET_BRIGHTNESS,
} pwmled_cmd_t;

/********************* pwmled.c *********************/

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/pwm.h>

#include "pwmled.h"

MODULE_LICENSE("Dual MIT/GPL");
MODULE_AUTHOR("Phlon | https://ixx.life");

#define PWMLED_PERIOD 1000000 // 脉冲周期固定为1ms

static struct {
struct pwm_device* pwm;
unsigned int brightness;
} pwmled;

long pwmled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case PWMLED_CMD_SET_BRIGHTNESS:
// 所谓调节亮度,就是配置占空比,然后使能pwm0
pwmled.brightness = arg < PWMLED_MAX_BRIGHTNESS ? arg : PWMLED_MAX_BRIGHTNESS;
pwm_config(pwmled.pwm, pwmled.brightness * 1000, PWMLED_PERIOD);
if (pwmled.brightness > 0) {
pwm_enable(pwmled.pwm);
} else {
pwm_disable(pwmled.pwm);
}
case PWMLED_CMD_GET_BRIGHTNESS:
return pwmled.brightness;
default:
return -EINVAL;
}

return pwmled.brightness;
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = pwmled_ioctl,
};

static struct miscdevice dev = {
.minor = 0,
.name = "pwmled",
.fops = &fops,
.nodename = "pwmled",
.mode = 0666,
};

int __init pwmled_init(void) {
// 请求PWM0通道
struct pwm_device* pwm = pwm_request(0, "pwm0");
if (IS_ERR_OR_NULL(pwm)) {
printk(KERN_ERR "failed to request pwm\n");
return PTR_ERR(pwm);
}

pwmled.pwm = pwm;
pwmled.brightness = 0;

misc_register(&dev);

return 0;
}
module_init(pwmled_init);

void __exit pwmled_exit(void) {
misc_deregister(&dev);
// 停止并释放PWM0通道
pwm_disable(pwmled.pwm);
pwm_free(pwmled.pwm);
}
module_exit(pwmled_exit);

该驱动会自动创建/dev/pwmled设备节点,然后应用层通过ioctl即可设置和获取灯的亮度等级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(int argc, char* argv[]) {
int fd = open("/dev/pwmled", O_RDWR);
int brightness = 0;
char key = 0;

while ((key = getchar()) != 'q') {
switch (key) {
case '=':
brightness += brightness < PWMLED_MAX_BRIGHTNESS ? 10 : 0;
break;
case '-':
brightness -= brightness > 0 ? 10 : 0;
break;
}

if (ioctl(fd, PWMLED_CMD_SET_BRIGHTNESS, brightness) < 0) {
perror("ioctl");
break;
}
}

close(fd);
return 0;
}

正如本文开头那张动图一样,用户只需要按+/-即可调节led的亮度,so easy~

小结

  • PWM即脉冲宽度调制,可以实时改变脉冲源的占空比和周期时长
  • 树莓派Linux内核默认不加载pwm通道,需要在/boot/config.txt中增加一行dtoverlay=pwm
  • pwm可以像gpio一样通过命令行对sysfs虚拟文件系统操作,即可控制pwm资源
  • 内核提供了标准的pwm接口,注意通道值的配置是以纳秒为单位
  • pwm技术应用范围非常广,务必牢牢掌握