0%

南北两宋(960-1279),元朝(1271-1368),两个朝代四百年,中国完成了从“天下之中”到“大中华”的转变。

宋人温文尔雅,能用钱解决的问题,绝不用刀枪。从宋的帝国版图来看,比前朝差远了,仅管辖着如今的四川、两广、沿海地区。但宋的政策开明,财政、文化、科技都达到了历史高点。就拿财政来说——国土仅大唐的三分之一GDP却是大唐的六倍之多。而你能想象得到吗,就是这样一个政府最后几乎是被穷死的。

再说元朝,这是个让人纠结的朝代,大家都还在争论元朝到底算外族入侵,还是继承中华文明。说外族入侵吧,当年的忽必烈的确认为自己是中国的皇帝;说文明朝代更迭吧,蒙古人统治时期,中华文明被践踏,百姓被分三六九等。宋朝有青花瓷、词曲、水浒、印刷术,以及一堆如王安石、司马迁这样的名人。那么大元朝呢?脍炙人口的人文思想,你能说出几样?也许只有成吉思汗和忽必烈。但他们到底还是蒙古人,到底属于蒙古帝国,是的,并非元王朝。蒙古不仅改变了中华文明的进程,也改变了全球秩序。从此,亚洲文明圈不再被中国主导,欧洲信仰得到一次清洗与统一,文明中心也一路向西。这个来也匆匆去也匆匆的蒙古,对这颗星球的后世影响之深,不容小觑。

宋元时期国家形式-图片来自维基百科

宋的诞生基本没什么悬念,故事很简单:晚唐时期由于藩镇割据加上宦官干政,帝国政府岌岌可危。而政府的禁卫军在当时一个叫陈桥驿的地方突然发动军事,拥立首领赵匡胤为帝,各种称呼万岁,各种黄袍加身。赵匡胤赶紧说:“使不得,使不得!”将士们积极回:“就你啦,就你啦!”就这样,赵匡胤称帝,改国号为宋。

当然,易中天在书中各种剖析案发细节,认为这根本就是赵匡胤自导自演的一出好戏。明明就是自己想当皇帝,非要搞得像是被逼的。陈桥兵变这个故事聚集了堪称奥斯卡最佳导演、最佳男一、最佳配角、最佳群演等老戏骨,就差颁奖典礼了。

另一个故事更耳熟能详——杯酒释兵权。话说赵匡胤掌权后,为了避免手下效仿自己也来个“黄袍加身”,就在一次酒宴上说:“你看看人家大唐,为啥亡了,还不就是藩镇的火力太猛,动不动就擦枪走火,天下哪能太平?藩镇一日强势,国家永无安宁啊!”说白了,就是各种威逼利诱,把将帅们比做藩镇,暗示他们交出兵权。当然,他成功了,宋朝也几乎每听过藩镇之争的事情。

与兵权一样,财政、行政、军事等重要权力全部划归中央,确切的说是皇权所有。但这与前朝是不一样的,集权的主要目的是为了防止内乱,之前的朝代更倾向于皇权代理,皇帝一个人忙不过来,所以雇一帮大臣来帮他分担。因此前朝的干政、架空皇权的情况很普遍。宋朝通过升级隋唐的制度,主基调是文官来主持朝政,武官专注军事,这让那些胸怀天下的士大夫终于可以和皇帝“共治天下”了。

得益于印刷术和造纸术的普及,获取知识的成本变得很低,甚至有黑心商贩都开始卖盗版书了。不论如何,以前富家子弟才能玩得起的游戏(考功名),现在穷苦人家也敢一试了。所以,宋朝不仅在科举方面取得空前成绩,科技、人文、艺术,也随着成本的降低而达到了巅峰和高潮。你想想,宋词、山水画、陶瓷技术、算术等,不都是这个时期的产物吗。更夸张的是,宋朝民间贸易极为发达,看看清明上河图就知道,由于买卖空前,政府铸币的速度赶不上百姓消费的速度,只能印一些纸钞票据来表示特定商品价值(我们中国人的“钞票”概念也就源于此)。哎,生意好到逼着政府玩起了证券期货,也是醉了。

此外,宋朝不仅延续了大唐的丝绸之路,在海洋贸易的成绩更是空前。那个时候与宋朝通商往来的国家有五六十个之多。主要货物包括丝绸、瓷器、纺织、五金、茶。当然,中国人同样进口香料、琉璃、象牙等各种奇珍异宝。

当然,还有民族融合,虽然不及大唐,但这个时期的各种外国人照样能在中国做生意、做官、安家落户,政府会给他们保驾护航。文化方面更是数不胜数,《水浒》/《资治通鉴》、包青天、精忠报国的岳飞、各种著名诗/词人。经济方面远超盛唐,正可谓财大气粗。这可真叫藏富于民呀。按道理,这么活跃的市场,这么富足的国家,政府的银子应该哗啦啦堆满国库才是,怎么最后还穷死了呢?

因为花钱的地方太多了!

宋朝的公务员是分两种的,“官”和“差”。官基本是头衔,挂个职领俸禄;差才是执行实际事务的。宋朝后期由于养的官员太多,百姓都视当官为铁饭碗,争相考功名,见谁都叫大官人,就好比我们现在见人都叫老板一样。所以一大帮做吃空饷的官员压在朝廷背上,压力不小啊。

此外,看看南宋时期的版图,北面就辽金、东有高丽、西有吐蕃、南有南诏,何况此时蒙古还没登场。以辽国为例,在萧皇后的率领下,辽国常年骚扰南宋,宋又没能力灭了他们,最终只能坐下来谈并签订著名的“澶渊之盟”,宋每年给辽三十万,秋毫无犯,用钱换来一百多年的和平。其他周边国家自然会眼红效仿。

即便花钱买和平,但总有人不吃这套吧,军队依旧要养。而且按宋人官差分离的逻辑,军队里存在吃空饷的酒囊饭袋。何况宋朝时期,军队经商依然成风,各种合法走私,贩卖军火,也是国家财产流失的方向。

总之,尽管宋朝税收很可观,但收税的速度赶不上花钱的速度,政府迟早要破产。国家已经到了不得不改革的地步,此时一个重要任务登场了——王安石。

王安石变法

确切地说,应该叫熙宁变法,只不过变法的一把手是王安石。变法是在宋神宗赵顼掌朝的熙宁年间发生的。朝廷内很多士大夫认为政府与民间就是一场零和博弈,税收越多,民间财富越少,于是纷纷要求宋神宗减税。这显然是一种政治正确的混账话,国库已捉襟见肘,还减税,你们还想不想要工资。所以宋神宗一心扶持王安石上位,因为他的思想更符合宋神宗,即通过变法使民间市场更活跃,把蛋糕做大,确保百姓钱包鼓鼓的同时增加税收,照样让政府富起来。因此,士大夫们看到的只是存量市场,而王安石看到了增量市场。

王安石提出了很多切实可行的方式:

  • 青苗法:国家向民间低息放贷,让贫苦农民有钱买种租地,等来年秋收再连本带利归还国家。这样农民有钱赚,国家有利息收,还抵制了可恶的高利贷。
  • 免役法:花钱即可免去当年服役,这里的役不仅仅是兵役,还要去各种政府部门当免费劳动力,占用百姓的时间和精力,自家的事业反而管不上。
  • 市易法:政府在物价下跌时增加收购,物价上涨时平价卖出,既增加财政收入,还能方式少数人操纵物价。
  • 保甲法:每家每户轮流出人在周围巡逻当保安,农忙时耕种,农闲时接受训练,节省军费,还能训练一支民兵。

诸如此类的变法共有十几种,从表面上看都能感受到,王安石的思想非常超前,不得不感叹古人已经对市场经济这么了如指掌了,还能提出国民双赢的思路。然而就是这样超前的变法,最终让沧桑遭了大罪,不论朝廷还是老百姓,都对他恨之入骨。恐怕除了皇帝,人人得而诛之。

为什么呢?描述下当时的场景,开封城外尘土飞杨的道路满是流亡的难民扶老携幼。他们饥寒交迫,只能吃树皮草更,为了偿还政府的强制借款利息,不得不典当妻子卖儿女,而政府还要从这笔买卖上想方设法抽税。

这一切被记录在一幅民间疾苦图中,上奏给宋神宗,皇帝看完潸然泪下。不久便撤换了王安石,司马光登场了。此时的他早已和王安石反目成仇,迅速且全盘否定了王安石变法,但正是这种雷厉风行,打得民间措手不及,再次让百姓受苦受难。

王安石变法失败是因为操之过急,脱离群众。司马光矫枉过正还是因为操之过急,脱离群众。两人都是为了天下苍生,结果却殊途同归。

或许王安石和司马光都是胸怀大志之人,他们才华横溢,已天下为己任。但我从书中看来,尤其是王安石,过于理想主义了。变法的目的是利国利民,但王安石刚愎自用,根本听不进谏言,他一心是想着如何废掉挡在他变法路上的大臣,强制推行变法。不论变法的思想是好是坏,他都表现出一种值得人反思的行为——漠视生命。

其实王安石的想法是好的,就是过于理想主义,面对如此庞大的国家,如此复杂的系统,即便看似天衣无缝的改革方案,未经调试就上线,就是不对人民群众负责。更何况在他的政治生涯里,是真的做到了大义灭亲,曾经的故友一一变为政敌,那些为他站台撑腰的人到最后都沦为变法的牺牲品。为了变法,为了自己的理想,任何反对他的人都要赶尽杀绝。当然,他绝对不是为了自己的荣华富贵,在读本书的时候,我可以明确感受到,他是真的心系天下,是的,只有天下,没有苍生。

也许,这就是变法失败的根源吧。

蒙古崛起

这段“历史”我们都很熟悉,主要归功于金庸的《天龙八部》《射雕英雄传》等小说。当然,小说只是取材于历史,历史并非小说。

我们知道大名鼎鼎的乔峰乔帮主是契丹人,而契丹就是常年盘踞如今的中国北方,北宋与其签订澶渊之盟,从而换来一百多年的和平。之后东北方的女真人崛起,就是后来的满族。先是一举南下灭了契丹,改国号为“金”。大金帝国又继续挥师伐宋,此刻的宋朝官员享乐空谈,军队忙于经商,加之改革失败,哪里是金人的对手。兵败后,宋朝皇帝赶紧让位给儿子,自己准备开溜,结果徽钦二帝被俘。皇室贵族大部分被金人蹂躏致死,又重新扶植傀儡政权,好在当时的康王赵构幸免于难,从开封一路逃到杭州,并在此建都,南宋开启。

这一系列就是著名的靖康耻,我们的郭靖和杨康也在小说里诞生了。率领金人的领袖叫完颜阿骨打,所以杨康之前叫完颜康么。那郭靖呢,当时正混在蒙古包里。那再来看看蒙古。

就我个人而言,蒙古汗国是蒙古汗国,元朝是元朝,不要混!

首先要搞清楚,蒙古人打的不是什么侵略战争,他们的战争动机就是战争本身。因此他们铁马金戈飞驰过的地方,无不烧杀抢掠、生灵涂炭,因为他们的只是为了掠夺。打个比方,蒙古人要的是现成的肉,不是什么养殖场。他们的战争是泯灭人性的,用书中的原话来说:“砖木结构的房屋统统化为灰烬,到处都是横七竖八的尸体,街道则因太多的人油变的滑溜溜的。灰蒙蒙的夏日夕照下,只看见满载战利品的车辆穿梭来往络绎不绝。”

南宋的灭亡理所当然“归功于”蒙古人,崖山之战值得铭记,它是南宋的最后一刻,对于中华文明的转折也很有代表性,甚至有“崖山之后无华夏”的说法。当时南宋早已成了流亡政府,元兵追至崖山,十多万军民死守到底。但被围困十多天的士兵早已无力抵抗。最后,文官领袖陆秀夫对刚被拥立的小皇帝说:“国事至此,唯有一死,不可受辱。”背起小皇帝跳海自尽。杨太妃、张世杰及其众多将士忠臣,紧随其后,跳海殉国。

这一跳,忽必烈坐稳中国皇帝,之后的明朝继承元的精神,清朝紧跟明的思想。皇家为了集权,开始培养中国人的奴性,从此伴君如伴虎,哪里还看得到朝堂之上敢和皇帝争的脸红脖子粗的局面,哪里还有如此开放开明的王朝。古代中国人的精神到底是怎样的?不知道了。也许对我们这一代人来说,也就对晚清的丧权辱国历史比较深刻,毕竟考试要考的嘛。

对于蒙古的这段历史,我个人是不怎么喜欢的,诚然,这只是我的个人偏见罢了。

蒙古在当时是刚从部落时代切换到部落联盟时代。在这种松散的同盟组织下,能让这片草原享誉世界的当然是那个大名鼎鼎的成吉思汗——铁木真。极盛时期的大蒙古疆域有3300多平方公理,什么概念,将近4个中国!而铁木真死后,蒙古人也把他带回故乡,埋在草原之下,一个谁也发现不了的地方,所有人为他献上最崇高的敬意,永远把他铭记在心。

成吉思汗到底属不属于中国我不知道,我只是看到在他在把世界都变成蒙古牧场的计划里,中国或者中华文明并没什么特殊的。也许只有忽必烈应该纳入中华王朝的历史,因为他的确在这片土地上生根发芽了,但是,似乎又没那么

成吉思汗死后,大蒙古被他的四个儿子(或者说四大派系)分裂成四大汗国。分为位于东欧、中亚、新疆等地,忽必烈统治的自然是蒙古总根据地和大中华地区。关于四大汗国各种统治纷争不属于中华历史范畴,唯一值得注意的是虽然四大汗国发源蒙古,但他们逐渐都被当地同化,比如伊利汗国的旭烈兀就皈依伊斯兰。当然这些不属于中华历史范畴,重点还是说说元朝。

忽必烈和他的亲戚们一样,从小受到汉文明熏陶,骨子里是蒙古人,表面上也有汉人思想,这也难怪他想做中国皇帝,但在他统治下的中国令人不悦。元朝建立后,百姓被分等级,由高到低分别是:蒙古人、异目人、汉人、南人。异目人是指那金发碧眼的外国人,唐宋时期波斯、阿拉伯、叙利亚等都很多人迁居过来;汉人特指南诏(云南)、契丹女真(东北)的人民;南人呢?南宋原住民。

蒙古对中国乃至世界的影响,最突出的莫过于民族文化的统一进程。但蒙古人自己恐怕不那么想吧?他们把种族分三六九等,并没有蔑视或侮辱其他民族的意思,蒙古人不是纳粹,这么做更多是防止自己被稀释。他们用粗暴的手段让元朝疆域的各人种、民族生活在一起,那么中华文明呢?

这便是元朝对中华历史交出的成绩,中国从之前的华夏文明进入了大中华时代。以前是以中原文明为主基调,与周边民族、部落、邦国相互交融,就好比蛮夷戎狄、五胡、契丹、女真等,不论周边文明如何野蛮,最终都是被相互同化后才纳入华夏文明圈的。现在不是了,多民族还没来得及彼此了解,就强制圈在一起,民族间融合程度低,但自此以后,大中华的多民族国家特性更为明显,民族内部有天然的身份认同,但民族之间的身份认同也必须靠认同“中华”。

我不知道如何评价元朝的历史。总之元朝之后,中国的版图得到空前的扩张,民族兼并似乎也不在只有文明融合这一条路可走。大家彼此按照原来的文化、风俗、习惯、信仰生活在这片土地上,谁也不刻意同化谁,我们彼此相信自己属于大中华。正如当今中国的模式,56个民族是一家。

本文源码:https://github.com/philon/rpi-drivers/tree/master/01-gpio-led

GPIO可以说是驱动中最最最简单的部分了,但我上网查了下,绝大部分所谓《树莓派GPIO驱动》的教程全是python、shell等编程,或者调用第三方库,根本不涉及任何ARM底层、Linux内核相关的知识。显然,这根本不是什么驱动实现,只是调用了一两个别人实现好的库函数而已,跟着那种文章走一遍,你只知道怎么用,永远不知道为什么。

所以本文是希望从零开始,在Linux内核下实现一个真正的gpio-led驱动程序,初步体验一下Linux内核模块的开发思想,知其然,知其所以然。

GPIO基础

General-purpose input/output(通用输入/输出),其引脚可由软件控制,选择输入、输出、中断、时钟、片选等不同的功能模式。以树莓派为例,我们可以通过pinout官网查看板子预留的40pinGPIO分别是做什么的。

FD10F639-BBC1-4AB8-938C-7C69F3D005B4.png

如上图,GPIO0-1、GPIO2-3脚,除了常规的输入/输出,还可作为I²C接口,GPIO14-15脚,可另作为TTL串口。

总之,GPIO平时就是个普通IO口,仅作为开关用,但开关只是为了掩人耳目,背后的复用功能才是它的真正职业。

三色LED电路

弄懂了GPIO原理,那就来实际操作一把,准备点亮LED灯吧!

先来看看原理图,为了区分三色灯不同颜色的LED,我特别用红绿蓝接入对应的RGB三个灯,黑线表示GND。

接线图

如图所示,三色灯的R、G、B正极分别接到树莓派GPIO的2、3、4脚,灯的公共负极随便接一个GND脚。因此,想要点亮其中一个灯,对应GPIO脚输出高电平即可,是不是很简单呐!

电路图

BCM2837寄存器分配

基于上述,要点亮LED只需要做一件事——GPIO输出高电平。如何通过程序让GPIO口输出高电平呢?

GPIO的控制其实是通过对应的CPU寄存器来实现的。在ARM架构的SoC中,所有的外围资源(寄存器)其实都是被映射到内存当中的,所以我们要读写寄存器,只需访问它映射到的内存地址即可。

那么问题来了,为什么不直接读写CPU的寄存器呢?因为现代的嵌入式系统往往都标配内存模块,处理器也带有MMU,所以其内部寄存器也就交由MMU来管理。

综上,我们现在要找出树莓派3B+这款芯片——BCM2837B0的GPIO物理内存地址。

这里不得不吐槽一下,我先是跑到树莓派3B+官方网址去找芯片资料,得知BCM2837其实就是BCM2836的主频升级版;我又去看BCM2836的资料,得知这只不过是BCM2835从32位到64位的升级版;我又去看BCM2835的芯片资料,然而里面说的内存映射地址根本就是错的……

要确定BCM2837B0的内存映射需要参考两个地方:

  1. BCM2835 Datasheet,但要留意,里面坑很多,且并不完全适用于树莓派3B。
  2. 国外热心网友的代码,仅包含GPIO驱动,没有太多细节

官方文档在第6页和第90页有这样一句话和这样几张表:

Physical addresses range from 0x20000000 to 0x20FFFFFF for peripherals. The bus addresses for peripherals are set up to map onto the peripheral bus address range starting at 0x7E000000. Thus a peripheral advertised here at bus address 0x7Ennnnnn is available at physical address 0x20nnnnnn.

BCM2835 GPIO 总线地址分配

GPIO 复用功能选择

我就直说吧,共6个关键要素:

  • 外围总线地址0x7E000000映射到ARM物理内存地址0x20000000,加上偏移,GPIO物理地址为0x20200000
  • GPIO操作需要先通过GPFSEL选择复用功能,再通过GPSET/GPCLR对指定位拉高/拉低
  • BMC2835共54个GPIO,分为两组BANK,第一组[0:31],第二组[32:53]
  • GPFSEL寄存器每3位表示一个GPIO的复用功能,因此一个寄存器可容纳10个GPIO,共6个GPFSEL
  • GPSET/GPCLR寄存器每1位表示一个GPIO的状态1/0,因此一个寄存器可容纳32个GPIO,共2个GPSET/GPCLR
  • ⚠️国外热心网友指出:树莓派3B+的GPIO物理内存地址被映射到了0x3F200000!

好了,现在结合电路图可推导出思路:

  1. R、G、B分别对应GPIO2、3、4,需要操作的寄存器为GPFSEL0/GPSET0/GPCLR0
  2. 要把三个脚全部设为“输出模式”,需要将GPFSEL0的第6、9、12都置为001
  3. 要控制三个脚的输出状态,需要将GPSET0/GPCLR0的第2、3、4脚置1

先点亮红灯

现在开始写Linux驱动模块,先不着急完整实现,这一步只是把其中的红灯点亮,为此我甚至把绿蓝线给拔了!

代码实现非常简单,就是在加载驱动时红灯亮,卸载驱动时红灯灭。

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
#include <linux/init.h>
#include <linux/module.h>
#include <asm/io.h>

#define BCM2837_GPIO_BASE 0x3F200000
#define BCM2837_GPIO_FSEL0_OFFSET 0x0 // GPIO功能选择寄存器0
#define BCM2837_GPIO_SET0_OFFSET 0x1C // GPIO置位寄存器0
#define BCM2837_GPIO_CLR0_OFFSET 0x28 // GPIO清零寄存器0

static void* gpio = 0;

static int __init rgbled_init(void)
{
// 获取GPIO对应的Linux虚拟内存地址
gpio = ioremap(BCM2837_GPIO_BASE, 0xB0);

// 将GPIO bit2设置为“输出模式”
int val = ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET);
val &= ~(7 << 6);
val |= 1 << 6;

// GPIO bit2 输出1
iowrite32(val, gpio);
iowrite32(1 << 2, gpio + BCM2837_GPIO_SET0_OFFSET);

return 0;
}
module_init(rgbled_init);

static void __exit rgbled_exit(void)
{
// GPIO输出0
iowrite32(1 << 2, gpio + BCM2837_GPIO_CLR0_OFFSET);
iounmap(gpio);

}
module_exit(rgbled_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Philon | ixx.life");

这段代码太简单了,以至于我觉得完全不需要解释,直接看效果吧。图中的命令:

1
2
philon@rpi:~/modules$ insmod rgbled.ko  # 亮
philon@rpi:~/modules$ rmmod rgbled.ko # 灭

红灯点亮效果

再点亮全部

其实点亮红灯后,绿蓝灯无非是改改地址而已,没什么难度。本文的目的是学习Linux驱动,点亮LED不过是驱动开发的感性认识,所以我决定把简单的问题复杂化😄。驱动主要为用户层提供了几种设备控制方式:

  1. 通过命令echo [white|black|red|yellow...] > /dev/rgbled直接控制灯的颜色
  2. 通过命令cat /dev/rgbled查看当前灯的状态
  3. 通过函数ioctl(fd, 1, 0)可独立控制每个灯的状态

说白了,用户层只须关心等的输出的颜色,屏蔽了具体的电路引脚及状态。

为此,我们需要把三色LED模块当作一个字符设备来实现,本文是驱动开发实战,所以更多的讲如何实现,有关字符设备的原理可以参考我的另一篇文章《ARM-Linux驱动开发四:字符设备》

驱动主要分为两大块:设备的read/write/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
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
// 三色LED灯不同状态组合
static struct { const char* name; const bool pins[3]; } colors[] = {
{ "white", {1,1,1} }, // 白(全开)
{ "black", {0,0,0} }, // 黑(全关)
{ "red", {1,0,0} }, // 红
{ "green", {0,1,0} }, // 绿
{ "blue", {0,0,1} }, // 蓝
{ "yellow", {1,1,0} }, // 黄
{ "cyan", {0,1,1} }, // 青
{ "purple", {1,0,1} }, // 紫
};

static void* gpio = 0; // GPIO起始地址映射
static bool ledstate[3] = {0}; // 三个LED灯当前状态

void gpioctl(int pin, bool stat)
{
void* reg = gpio + (stat ? BCM2837_GPIO_SET0_OFFSET : BCM2837_GPIO_CLR0_OFFSET);
ledstate[pin-2] = stat;
iowrite32(1 << pin, reg);
}

// 通过文件读取,得到当前颜色名称
ssize_t rgbled_read(struct file* filp, char __user* buf, size_t len, loff_t* off)
{
int rc = 0;
int i = 0;

// 当文件已经读过一次,返回EOF,避免重复读
if (*off > 0) {
return 0;
}

// 根据当前三个LED的输出状态,找到对应颜色名,返回
for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) {
const char* name = colors[i].name;
const bool* pins = colors[i].pins;

if (ledstate[0] == pins[0] && ledstate[1] == pins[1] && ledstate[2] == pins[2]) {
char color[32] = {0};
sprintf(color, "%s\n", name);
*off = strlen(color);
rc = copy_to_user(buf, color, *off);
return rc < 0 ? rc : *off;
}
}

return -EFAULT;
}

// 通过向文件写入颜色名称,控制LED灯状态
ssize_t rgbled_write(struct file* filp, const char __user* buf, size_t len, loff_t* off)
{
char color[32] = {0};
int rc = 0;
int i = 0;

rc = copy_from_user(color, buf, len);
if (rc < 0) {
return rc;
}

*off = 0; // 每次控制之后,文件索引都回到开始

// 根据用户层传来的颜色名,找到对应引脚状态,输出
for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) {
const char* name = colors[i].name;
const bool* pins = colors[i].pins;
if (!strncasecmp(color, name, strlen(name))) {
gpioctl(LED_RED_PIN, pins[0]);
gpioctl(LED_GREEN_PIN, pins[1]);
gpioctl(LED_BLUE_PIN, pins[2]);
return len;
}
}

return -EINVAL;
}

// 通过ioctl函数控制每个灯的状态
long rgbled_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
if (cmd >= 2 && cmd <= 4) {
gpioctl(cmd, arg);
} else {
return -ENODEV;
}

return 0;
}

const struct file_operations fops = {
.owner = THIS_MODULE,
.read = rgbled_read,
.write = rgbled_write,
.unlocked_ioctl = rgbled_ioctl,
};

关于读/写/控制这三种操作的代码实现,看似复杂,其实都很容易理解,无非就是通过copy_to_usercopy_from_user两个函数,实现内核层与用户层之间的数据交互,剩下的事情不过就是在colors结构体数组中进行遍历和比对而已。

然后是字符设备注册、GPIO功能配置等内容的实现。每种字符设备都需要唯一的主设备号和次设备号,设备号可以静态指定或动态分配,原则上建议由内核动态分配,避免冲突。

字符设备的创建有很多种思路,普通字符设备、混杂设备、平台设备等,它们都是内核提供的编程框架。例如GPIO这类设备,内核其实是有专门的gpio类,但为了更好的学习驱动开发,别着急,一步步来,先从最简单的开始(因为难的我也不会)。

下边的代码主要看cdev_xxx相关的部分即可,驱动加载时配置好GPIO映射,注册字符设备,获取设备号;驱动卸载时,取消GPIO映射,释放设备号,注销字符设备。

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
static dev_t devno = 0;   // 设备编号
static struct cdev cdev; // 字符设备结构体

static int __init rgbled_init(void)
{
// 映射GPIO物理内存到虚拟地址,并将其置为“输出模式”
// 代码写得比较丑,解释以下:
// 就是先把三个GPIO的“功能选择位”全部置000
// 然后再将其置为001
int val = ~((7 << (LED_RED_PIN*3)) | (7 << (LED_GREEN_PIN*3)) | (7 << LED_BLUE_PIN*3));
gpio = ioremap(BCM2837_GPIO_BASE, 0xB0);
val &= ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET);
val |= (1 << (LED_RED_PIN*3)) | (1 << (LED_GREEN_PIN*3)) | (1 << (LED_BLUE_PIN*3));
iowrite32(val, gpio);

// 将该模块注册为一个字符设备,并动态分配设备号
if (alloc_chrdev_region(&devno, 0, 1, "rgbled")) {
printk(KERN_ERR"failed to register kernel module!\n");
return -1;
}
cdev_init(&cdev, &fops);
cdev_add(&cdev, devno, 1);

printk(KERN_INFO"rgbled device major & minor is [%d:%d]\n", MAJOR(devno), MINOR(devno));

return 0;
}
module_init(rgbled_init);

static void __exit rgbled_exit(void)
{
// 取消gpio物理内存映射
iounmap(gpio);

// 释放字符设备
cdev_del(&cdev);
unregister_chrdev_region(devno, 1);

printk(KERN_INFO"rgbled free\n");
}
module_exit(rgbled_exit);

代码基本就是这样。来看看效果吧,操作指令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 编译驱动,拷贝至开发板
philon@a55v:~/drivers/01-gpio_led$ make
philon@a55v:~/drivers/01-gpio_led$ scp rgbled.ko rgbled_test rpi.local:/home/philon/modules

#----------------------以下是开发板操作----------------------
# 加载驱动 & 查看模块的主从设备号
philon@rpi:~/modules$ sudo insmod rgbled.ko
philon@rpi:~/modules$ dmesg
...
[ 106.818009] rgbled: no symbol version for module_layout
[ 106.818028] rgbled: loading out-of-tree module taints kernel.
[ 106.820307] rgbled device major&minor is [240:0] 👈主从设备号

# 根据动态分配的设备号创建设备节点
philon@rpi:~/modules$ sudo mknod /dev/rgbled c 240 0

philon@rpi:~/modules$ sudo sh -c "echo green > /dev/rgbled" # 打开绿灯
philon@rpi:~/modules$ sudo ./rgbled_test b 1 # 再打开蓝灯
philon@rpi:~/modules$ sudo cat /dev/rgbled # 查看当前颜色
cyan #青色

PS:动态图,只是被我设置得比较慢,别着急换台呀!😂

三色LED驱动最终效果图

Large Interconnected Data Belongs to a Database

如果你的应用程序要处理一个大型、持久、互联的数据集,毫不犹豫地把它们存到关系型数据库里。在过去,RSBMS(关系型数据库)使用起来很昂贵、稀缺、复杂,且最为笨重。但这已经不在是问题了。现如今,关系型数据库系统非常容易找到——就像你的系统里已经安装了一到两款。一些非常强大的RDBMS,如MySQL和PostreSQL,都是开源软件,所以开支不在是问题。更有意思的是,嵌入型数据库系统可以作为一个库链接到你的应用程序里,几乎不需要安装和管理——其中著名的开源软件是:SQLite和HSQLDB。这些系统都非常之高效。

如果你应用程序的数据已经大到超过系统内存了,RDBMS表的索引表现将比映射集合类型的库要快出几个数量级,这将破坏虚拟内存页。现代数据库产品可以根据你的需求轻松扩展。只要你需要,可以将嵌入型数据库扩展到大型数据库系统中。之后,你还可以从免费、开源的产品切换到支持更好或更强大的专有系统中。

一旦掌握了SQL,编写以数据库为中心的应用程序将会很愉快。当你正确规范地将数据存储到数据库之后,就可以通过高效可读的SQL查询来提取内容了;不需要写任何复杂的代码。同样的,单条SQL命令也可以做到复杂的数据改变。对于一次性修改——比如组织持久化数据的改变——你甚至不需要编写代码:只需启动数据库直观的SQL界面。相同的界面还允许你试验性查询,以避免常规编程语言的编译-编辑循环。

围绕RDBMS建立的代码还有个优点,就是处理数据元素间的关系。你可以通过声明的方式描述数据的一致性约束,以避免在边缘情况下忘记更新数据而获得悬空指针的风险。例如,你可以指定,当一个用户被删除后,那个用户发送的消息也应该被删除。

任何时候只要你想,都可以在数据库的实体间创建链接,简单地通过创建索引即可。不需要对类型字段做昂贵又大范围的重构。此外,围绕数据库的编程还支持多应用安全访问。这就可以很轻松地更新应用以实现并发访问,也可以使用最适合的语言和平台编写程序的部分代码。例如,你可以在JAVA中编写基于WEB的XML后段,在Ruby中审计脚本,在Processing中编写可视化界面。

最后,记住RDBMS会使尽浑身解数优化你的SQL命令的,让你专注于应用程序的功能优化,而不是算法调优。高级点的数据库甚至会充分发挥多核性能优势。随着技术的提升,你的应用程序表现也会更好。

本文源码:https://github.com/philon/rpi-drivers/tree/master/00-hello

最近打算利用手里的树莓派3B+及各种丰富的扩展模块学些一下嵌入式Linux驱动开发。我计划实现LED、温湿度传感器、陀螺仪、PS遥感、红外模组、超声波模组、光敏传感器等驱动,其目的是为了学习嵌入式Linux驱动开发的思想,以及常见的接口及模块的原理。我会把整个学习过程整理为《树莓派驱动开发实战》。当然,我的初衷是学习ARM-Linux驱动开发,如果不是必须,我不会可以强调硬件平台,而是更侧重于驱动编程和电路设计。

我的开发环境如下:

  1. 宿主机:华硕A55VM | Ubuntu18.04
  2. 开发板:Raspberry 3 Model B+ 2017
  3. 编辑器:Visual Studio Code | C/C++扩展

由于文章中会存在大量命令脚本的引用,先做个声明:

1
2
3
philon@a55v:~$ # 表示在宿主机敲出的命令

philon@rpi:~$ # 表示在开发板敲出的命令

此外,如果不是必须,我不会拿串口调试开发板,一律ssh!

准备工作

Linux驱动开发需要准备几样东西:

  • Datasheet:硬件手册,用于了解目标平台的规格/寄存器/内存地址/引脚定义等
  • 编译器:用于编译目标平台的驱动源码,嵌入式开发,一般用交叉编译器
  • 内核源码:编译驱动依赖于内核,且必须与目标平台系统内核版本一致
  • 外围电路原理图:连怎么走线都不知道,还开发个球啊!

本文不涉及真实的模块驱动开发,因此具体的外围电路等实际开发的时候再看吧,先把前三样搞到手。

树莓派3B+硬件资料

由于官网没有配套的树莓派3B+Datasheet,我只能尽可能从官网其他地方把硬件资料凑齐。任何有关硬件的资料都优先从官网找:https://www.raspberrypi.org/documentation/hardware/raspberrypi/

树莓派3B+硬件介绍

  • 处理器是BCM2837B0,64bit四核1.4GHz的Cortex-A53架构
  • 双频80211ac无线和蓝牙4.2
  • 千兆有线网卡,且支持PoE(网线供电)

电路原理图下载:https://www.raspberrypi.org/documentation/hardware/raspberrypi/schematics/rpi_SCH_3bplus_1p0_reduced.pdf

宿主机安装交叉编译器

交叉编译器选择是有套路的:<arch>-<vendor>-<target>

例如:

  • arm-linux-gnueabi-gcc 表示arm32位架构,目标可执行文件依赖Linux+glibc
  • arm-linux-gnueabihf-gcc 表示arm32位/硬浮点数处理架构,其他同上
  • aarch64-linux-gnueabi-gcc 表示arm64位架构,其他同上

所以在选择交叉编译器之前要先确定开发板的处理器及操作系统环境:

1
2
philon@rpi:~ $ uname -a
Linux rpi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

注意最后的armv7,尽管RPi-3B+的处理器平台是64位4核,不过它的操作系统是32位的,所以应该安装arm32位glibc的交叉编译器。

1
philon@a55v:~$ sudo apt install gcc-arm-linux-gnueabihf

宿主机构建内核源码

Linux驱动模块的编译依赖于内核源码,而且要注意版本问题。之前在开发板上查看Linux版本时已经知道其运行的内核是Linux rpi 4.19.42-v7+。所以最好是去官网下载对应版本号的内核源码。

1
2
3
4
5
6
7
8
9
10
# 下载内核源码
philon@a55v:~$ wget https://github.com/raspberrypi/linux/archive/rpi-4.19.y.tar.gz
# 解压,进入内核目录
philon@a55v:~$ tar xvf rpi-4.19.y.tar.gz && cd linux-rpi-4.19.y
# 清理内核
philon@a55v:~/linux-rpi-4.19.y$ make mrproper
# 加载RPi-3B+的配置
philon@a55v:~/linux-rpi-4.19.y$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2709_defconfig
# 预编译
philon@a55v:~/linux-rpi-4.19.y$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare -j8

【题外话】:很多教程都是先把内核完整编译一遍,再拷贝各种头/库文件到目标平台,其实没必要,预编译内核即可。从内核官方文档可以看到这样一段话:

=== 2. How to Build External Modules
To build external modules, you must have a prebuilt kernel available
that contains the configuration and header files used in the build.

An alternative is to use the “make” target “modules_prepare.” This will
make sure the kernel contains the information required. The target
exists solely as a simple way to prepare a kernel source tree for
building external modules.
NOTE: “modules_prepare” will not build Module.symvers even if
CONFIG_MODVERSIONS is set; therefore, a full kernel build needs to be
executed to make module versioning work.

简而言之,如果要构建外部驱动模块,内核必须有相关的配置及头文件。可以用“modules_prepare”准备内核源码树,这也仅仅用于构建外部驱动模块,但不会生成“Module.symvers”。

第一个树莓派驱动

入门例子当然是留给HelloWorld啦!

创建工程目录:

1
2
3
4
# 创建目录,进入
philon@a55v:~$ mkdir -p drivers/00-hello && cd drivers/00-hello
# 创建驱动模块源码及Makefile
philon@a55v:~/drivers/00-hello$ touch hello.c Makefile

hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
printk("hello kernel\n");
return 0;
}
module_init(hello_init);

static void __exit hello_exit(void)
{
printk("bye kernel\n");
}
module_exit(hello_exit);

MODULE_LICENSE("GPL v2"); // 开源许可证
MODULE_DESCRIPTION("hello module for RPi 3B+"); // 模块描述
MODULE_ALIAS("Hello"); // 模块别名
MODULE_AUTHOR("Philon"); // 模块作者

Makefile

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
# 模块驱动,必须以obj-m=xxx形式编写
obj-m = hello.o

KDIR = ../linux-rpi-4.19.y
CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

all:
$(MAKE) -C $(KDIR) M=$(PWD) $(CROSS) modules

clean:
$(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean
````

```sh
# 编译内核
philon@a55v:~/drivers/00-hello$ make
# 将内核模块ko远程复制到开发板
philon@a55v:~/drivers/00-hello$ scp hello.ko rpi.local:/home/philon/modules/

# 开发板加载hello模块
philon@rpi:~/modules $ sudo insmod hello.ko
# 卸载内核
philon@rpi:~/modules $ sudo rmmod hello
# 查看内核打印信息
philon@rpi:~/modules $ dmesg | tail -5
[ 1176.602268] Under-voltage detected! (0x00050005)
[ 1182.842326] Voltage normalised (0x00000000)
[ 4731.671023] hello: no symbol version for module_layout
[ 4731.671042] hello: loading out-of-tree module taints kernel.
[ 4731.672307] hello kernel 👈看到没,就是它了!
[ 5010.908453] bye kernel

Know Your Next Commit

我轻轻拍着三个程序员的肩膀并问他们在做什么。“我在重构这些方法”,第一个程序员说。“我在为这些web动作添加一些参数”,第二个程序员说。而最后那个程序员却说:“我正在研究这个用户故事。”

前两个程序员貌似都专注于细节,只有第三个能看到更大的画面,显然后者的聚焦更棒。然而,当我问何时提交什么,情况就发生了变化。前两个十分清楚会涉及到哪些文件并在一小时左右完成。第三个程序员的回答基本上是这样的:“噢!我应该要准备几天时间。我可能要增加几个类,并改变现有的服务机制”。

前两个缺乏全局视角。他们选择由生产方向引领的任务,并可以在几小时内完成。完成这些任务后,他们会选择实现新功能或者重构。所有代码都有明确的用途、范围、可实现的目标。

第三个程序员没有分解问题,同时处理所有部分。他无法采取任何措施,基本上只能投机性的编程,寄希望能够让某些点达到可以提交的程序。最终这个花长时间讨论后才开始写的代码,极有可能与终极解决方案相差甚远。

如果前两个程序员的任务会花费两小时以上,他们会怎么做?意识到自己承担太重后,他们最有可能抛弃他们的变更,定义更小的任务,然后重新开始。因为保持原样的话会找不到重点,从而导致投机性代码提交进仓库。相反,变更被抛弃了,但对其中的洞悉却得到保留。

第三个程序员则可能继续瞎猜,拼命尝试给他的变更打补丁,以拼凑成可以提交的内容。毕竟,你又不能抛弃已经完成变更的代码——那就等同于之前的努力白费了,不是么?悲剧的是,不抛弃这些代码将进一步导致那些目标不明确的诡异代码提交到仓库。

某些时候,即使“聚焦提交”的程序员也可能无法找到自认为可以在两小时内完成的有用内容。然后,他们会进入投机模式,开始调戏代码,一旦有所洞悉,他们就会回到正轨,并抛弃之前的变更。即便这些看似凌乱的黑客会议也是有目的的:为了了解定义出能够形成高效任务的代码。

知道你的下次提交。如果你无法完成,就抛弃这次变更,然后通过你的见解,定义靠谱的任务。如果有需要,就做那些投机性上演,但要注意别掉到投机模式的坑里。不要提交猜测性的工作到你的仓库里。

隋唐(581年-907年),是中国经历了400年的长期分裂后迎来的一次大统一,而且是享誉古今中外的大唐盛世。那么,为什么是唐朝?这个时代具有什么样的特色让它不可一世?这一切都值得我们去探索。

隋唐接力棒

隋朝之前是南北朝,南北朝是一个统称:

  • 南朝是之前西晋的延续,由汉人统治的中国,包括宋、齐、梁陈等四朝;
  • 北朝是五胡乱华的后续,包括北魏、东魏、西魏、北齐、北周等五朝;

上述的朝代即包括同时期的不同地方分裂,也包括不同时期的朝代更迭。还有,晋朝传递下来的分封和士族世袭等制度或观念,在南朝得到延续,导致贵族越来越昏庸无能,政治腐败,国家战乱连连。所以说就一个字——乱!

混血儿杨坚

杨坚是鲜卑化的汉人,效力于北周皇帝,各种南征北战,之后升官拜相。集军政大权于一身后,逐步废掉北周皇帝,自立为帝,建立隋朝。接着北灭突厥,挥师南下,伐西梁、亡陈朝,自此统一全国。

一统江山之后,隋文帝杨坚还干了几件了不起的事:

  • 【府兵制——军改】平时为民,战时为兵;兵不识将,将不知兵。既解决了绝响,由杜绝了军阀。
  • 【五省六部制——权利制衡】:废除九品中正制,各省间负责最高指示,各部负责国家业务划分,没有谁能独揽大权。
  • 【科举制——人才选拔】:通过考试选拔人才,不问出身门第。
  • 【施仁政——利国利民】:通过再三减税、罢盐/酒专营,推行均田制,导致经济和人口大幅增长。

这是从人治到法治的转变,通过各种制度与制衡确保国家行政机器能有效运转。隋文帝杨坚淡化了统治阶级的存在感,换而言之,不论谁走马上任,只要不昏庸至极,都能在流程制度上不出什么大乱子。

李唐登场

隋文帝死后,儿子杨广即位,也就是大名鼎鼎的隋炀帝,“炀”这个溢号是唐朝李渊给封的,意思就是昏庸无能至极的暴君,是对死后皇帝最狠的差评。当然,这里有刻意抹黑对手的嫌疑,杨广没有那么不堪,但他真的很像秦始皇。

具体说来,隋炀帝在位时,打通南北大运河(从杭州到北京,全场2700多公里),重建东都洛阳,东征西讨,西至如今的新疆,东至如今的朝鲜,北至如今的蒙古,中华版图翻倍。然而,这一切丰功伟绩的背后是无数百姓的世故血汗。

如此激进又冷血的皇帝,对于当时的人来说,只能是民怨四起,本效力于隋朝的李渊反了,在晋阳发起兵变。晚年的隋炀帝已无力控制全国,李氏父子出生入死一口气推翻了大隋政权。

自此,李渊称帝,国号为唐。但是在立太子问题上出岔子了,唐高祖立嫡长子李建成为太子,从自古继承规矩而言没问题。可大唐的江山有一半是他李世民打下来的,结果便宜了哥哥,有没有搞错!所以李世民一手炮制玄武门之变,手足相杀,逼父禅让。此后便是唐太宗李世民的时代,华夏文明也迎来自古以来的最强时代!

开放,是一种大国自信

隋唐和秦汉发展轨迹太像了,秦、隋都作为巨大革命的试验品二世而亡,紧随其后的汉、唐不但继承了他们的衣钵,更是迎来了空前盛世。最强大唐帝国的版图比现代中国还要辽阔。周边的蛮夷戎狄无不臣服于大唐,当年的李世民不仅仅是中华的天子,更是周边游牧民族心中的天可汗(可汗是最高领袖的意思,而天可汗似乎更上一层)。

当然,这仅仅是唐朝在军事和外交方面的成绩,在笼络人才方面也真是做到极致了。举个例子:日本的仲麻吕、阿拉伯的李彦升、朝鲜的崔致远都先后在中国考取进士…一个国家的重要公职人员居然是个老外,放在今天你能想象吗?关键人家对中华的儒家思想、文化历史了解的比你还深,吟诗作对更是信手拈来。能多次出现这种现象绝非偶然,是整个中亚圈对中华文明的崇敬与向往,实际上唐朝年间有大批的国外使者留学生常驻中国,学习中华文化(这背后也可能是大唐常年对外国使者高规格免费招待的政策所赐)。

民间社会呢?丝绸之路的强盛,国土向西的扩张,对外来人口的宽松政策,导致整个长安城直接进化为全球最强悍的国际大都市,不会两句外语都不好意思和人打招呼。长安街有两个商圈,一个叫东市一个叫西市,全球的奇珍异宝不在东便在西,老百姓的日常购物也被称为“买东西”。唐朝女子穿起胡服,尽显婀娜曲线,后来更是学着波斯、吐蕃、中亚等异国风情,戴耳环、梳髻堆、化面赭(一种脸妆),穿着也越露越多。不论男女都喜欢骑马踏青打马球。长安街边各种老外开的西餐厅,各种高档会所组织派对,跳着胡旋舞,奏起龟兹乐。酒过三巡,还有一个个名闻天下的诗人慷慨激昂,真是葡萄美酒夜光杯啊~

唐朝的开放是什么?政策的宽松,思想的解放,身份的认同,文化的融合。是的,唐朝总体上给人一种“来者不拒”的感觉,不论你要吟诗骂国、诵经传教、纸醉金迷、偷师学艺、悬梁刺股,请自便。大唐不介意文化输出还是输入,也许来者不善各怀鬼胎,但民间盛世空前,一路路金戈铁马,英姿煞爽的天子/天可汗,你奈我何?是的,唐朝不是因为开放而强大,相反,是强大的唐朝自然而然走向了开放。开放,是一个国家对自己富强的认同,是最低调的炫富行为。

大唐的漏洞

我们知道,唐朝实行的三省六部制,分权制衡的策略,是站在法治的角度把国家看错是一部高效运转的行政机器。所以各种政策的制定都有一个宏观目标,即不论职位上的人是谁,包括皇帝在内,他的权力都会受到相应的制约,以确保不会范下滔天大罪。相比于隋唐之前,这明显是从人治到法治的进化。但唐朝的制度却留下了三大bug:女人、宦官、藩镇。

我们把天下比做一个家,家里的兄弟就好比整个朝廷官僚体系,唐朝的制度就是为这些兄弟量体裁衣制定的,就是为了防止将来分家产鱼死网破。结果到好,兄弟们的行为都被限制住了,家里的儿媳妇开始冒头。到了后期,家里的保姆管家也开始气焰嚣张了。紧接着各方亲戚也上蹿下跳,这个家也就摇摇欲坠了。

女人就如武则天这类的后宫儿媳妇,保姆管家就是朝内的太监宦官,亲戚就是藩镇到军阀。

其实可以这么理解,在古代,天下的产权都归皇帝一个人,但产权是产权,使用权是使用权啊。皇帝可以把自己的权力分给朝臣们代理行使,唐朝的制度只是避免了名以上的产权拥有者和代理行使者的纠纷,实际上参与代理皇权的人还有很多,一句枕边话,一个确认过的眼神,都可以对皇帝的决策产生重心偏离。久而久之,皇帝被架空,重臣也可以被架空。

我们不好这件事是对是错,但也幸好这样,中国才得以有个女人当皇帝,为整个中华历史涂抹一些不一样的色彩。

武则天

我个人觉得看待武则天需要分两个角度:

  • 她是一个心狠手辣的政客,但凡敢挑战她权力地位的人,必将遭到最阴毒的攻击,在她实质掌权期间,被迫害致死的官员怕是上千了,不论这些人是君子还是小人,挡武者死!
  • 她是一代圣明君主,在她掌权下的中国大力推动科举,消灭门阀,让社会底层人才有了出头之日;同时轻徭薄赋,社会人口和经济都出现高速增长;中国的文化思想也在该时期得到最大限度的绽放。

可以说是历史选择了武则天,因为我们知道这个刚入宫的小姑娘野心勃勃,但她伺候的却是唐太宗李世民,在这个胸怀天下的天可汗眼里,一个女人算什么。李世民驾崩后,武则天被迫削发为尼进入感业寺,这就相当于政治生涯到头了。幸好她把赌注押向了那个唯唯诺诺的青少年李治,李治登基后,她按部就班回到宫中,迫害王皇后和萧淑妃,自己终于成为武皇后,这真是绝处逢生啊。

武皇后得势,以她的政治野心及权谋,搞定一个李治跟玩儿似的。于是她逐步从幕后到台前,与皇帝并称“二圣”。抚皇帝、扰后宫、废太子、杀宰相、用小人,从此平步青云,朝堂之上被儒家思想熏陶的大臣胆敢看不惯,直接把他废了。人是越杀越多,到最后整个朝廷都被她杀得没脾气了,唐高宗李治也嗝屁了。她的亲儿子立了又废,废了又杀,直到后来她的小儿子磕头认怂不敢当皇帝。只见她大喝一声:还有谁!!

一代女皇帝闪亮登场了,她机关算尽为自己的天子之路扫清障碍。上位后,武则天接着杀人,但杀的是那些曾今用过的小人。是的,当上皇帝后,她慈眉善目菩萨心肠,要还世间一个公道。所以武则天的政治生涯可分为两部分,前半生尔虞我诈,后半生安邦定国。大唐在她的手上照样太平盛世,和男性皇帝相比,恐怕只是少了些许残暴,多了几分仁慈吧。

但是,人终有一死。武则天晚年面另一个艰难的抉择:是武周还是李唐?选择改朝换代,她无疑要背上王莽篡汉般的千古骂名;选择还政于李家,那她这辈子又折腾个什么劲呢。不过她最终还是选择做李家的媳妇,给自己立了个无字碑。或许是想把评价留给后人;又或者,一个登峰造极的女人,还有必要为自己论证什么吗?

安史之乱,盛极而衰

女人的戏唱完了,就该轮到宦官和藩镇登场了。其实安史之乱没有太多内容,但它作为大唐盛极而衰的分水岭,自然有特殊的意义。

安史之乱是唐玄宗李隆基年间发生的事情,那个时候的中国幅员辽阔,但与周边少数民族的关系不一定融洽,北面突厥(今蒙古),西面吐蕃(今西藏、新疆),高句骊(朝鲜),以及丝绸之路上的今天阿拉伯、印度等地区,都多少有些摩擦。天高皇帝远,大唐设立藩镇,就是派重兵把守边疆,但赋予军权的同时,还把部分行政权也交了出去,其目的是让指挥官自行解决军饷和当地民生问题,不要让朝廷操心了。想法是好的,可这种行为其实和封建时代的诸侯分封有何区别?不过差了一张产权证而已。而且,边疆的部队及战斗力远远强于朝廷,所谓藩镇,就是军阀。

安禄山和史思明想大唐发动进攻,不过是吹响了大唐时代的战国号角,从此朝廷无宁日。雪上加霜的是,宦官乱政,这便是唐朝制度上的第三个漏洞。宦官们借着亲近皇帝的机会,左右决策。这可比女人干政要恶劣的多,媳妇再霸道好歹也是自家人,多少还会为自己的家着想。这保姆可是领死工资的,他要是掌权,还不能捞一笔是一笔,家庭兴衰,关他屁事。

所以在此后一百多年了,各种傀儡皇帝相继登场,朝廷内部开始搞起派系斗争。为官的目的不是整死各位,就是被各位整死。这样的统治阶层,能够维持一百多年,不惊讶么?其实原因有两方面:一、藩镇军阀间相互混战,给了朝廷苟延残喘的空间;二、唐朝的行政制度设计的却是好,至少在最差情况下还能勉强维持运行。

好了,易中天的隋唐这部分看完了。总而言之,唐的确是个了不起的朝代,在我看到极盛大唐的介绍时,真有一种梦回唐朝的冲动。

Know Your Limits

人需要了解自己的局限性——Dirty Harry

你的资源受限了。你只有那么点的时间和钱去工作,还需要保持你的知识、技能和工具更新迭代。你也只能是更努力、快速、灵活、更长时间地工作。你的工具也如此而已。你的目标平台性能也就如此。因此你不得不敬畏这种资源限制。

如何敬畏呢?了解你自己,了解你周围的人,了解你的预算,了解你所拥有的。尤其是作为一个软件工程师,应该了解你的数据结构和算法的时间/空间复杂度,你系统的架构及典型性能。你的工作就是创造一次软件和系统的最佳结合。

时间和空间复杂度函数给出的O(f(n)),这个n等同于输入参数趋近于无穷时,n增长的时间和空间的情况。主要的f(n)复杂度类别包括:ln(n)、n、n*ln(n)、n^e、e^n等。如下图所示,这些函数表明,档n越大,O(ln(n))总是小于O(n)O(n*ln(n)),也比O(n^e)O(e^n)要小得多。正如Sean Parent所说,对于可实现的n,所有复杂度类别中,要么接近常数,要么接近于线性增长,要么接近于无穷。

复杂度的分析是根据抽象机器来衡量的,但软件却运行在真实的机器上。现代计算机系统被组织为物理机和虚拟机两种架构,包括运行语言、操作系统、CPU、缓存、RAM、硬盘、网络等。下表展示了一个典型的网络服务的随机访问时间和存储能力的限制。

类型 访问时间 存储容量
寄存器 < 1ns 64b
缓存 64B
- 1级缓存 1ns 64KB
- 2级缓存 4ns 8M
RAM 20ns 32GB
磁盘 10ms 10TB
局域网 20ms > 1PB
互联网 100ms > 1ZB

要注意(不同设备)存储能力和访问速度会有几个数量级的差异。但在我们系统的每个级别都采用大量的缓存和预测来隐藏这种差异,可惜它们只有在访问可预测时才能正常工作。当缓存未命中,系统将发生颠簸。例如,随机检查硬盘的每个字节要花费32年。甚至随机检查RAM的每个字节都要花费11分钟。随机访问时不可预测的,那什么可以?这依赖于系统,但重新访问最近使用和顺序访问才是常态。

算法和数据结构所使用的缓存在效率方面各有不同,例如:

  • 线性搜索有比较好的预测,但需要O(n)次比较。
  • 二分法搜索排序数组需要O(log(n))次比较。
  • van Emde Boas树则需要O(log(n))次和cache-oblivious模型。

如何选择呢?通过最后的分析测量。下表显示了在我的电脑上,通过这三种方法搜索64位整数数组所需时间(纳秒):

数组长度 线性 二分法 vEB
8 50 90 40
64 180 150 70
512 1200 230 100
4096 17000 320 160
  • 线性搜索在小数组时很有竞争力,但在大数组情况下就不行了。
  • vEB凭借可预测访问模型赢得胜利。

1980年代,我们的编程环境没有比文本编辑器更好的了…如果我们幸运的话。现如今是被认为理所当然的语法高亮,是一种奢侈品,不是每个人都能享用的。漂亮打印器为了把我们的代码格式化好看,通常不得不运行外部工具来纠正间隔。调试器也是单独的程序,用于逐步调试代码,但需要使用很多隐秘的快捷键。

来到1990年代,为程序员提供更好和更实用的工具,驱动着公司们开始挖掘潜在收入。继承开发环境(IDE)把编译器、调试器、漂亮打印器、以及其他工具整合进之前编辑器。在此期间,菜单概念和鼠标也开始流行起来,这就意味着开发者不再需要掌握那些隐秘的组合键。他们通过菜单就可以非常容易地选择他们的命令。

21世纪,IDE已经很普遍了,公司们为了获取其他市场份额甚至免费赠送它们。现代IDE都具备一组令人赞叹的功能。我最喜欢的就是自动重构,特别是“提取方法”,我可以在方法中选择和转换一个代码块。重构工具将提取所有方法需要的参数,让修改代码变得非常简单。我的IDE甚至会检测其他的代码块是否能够替换成此方法,然后问我是否也将它们替换。

现代IDE另一个惊人的功能是可以在公司内部强制风格样式。例如Java,一些程序员开始将所有参数都设置为final(在我看来,这是浪费时间)。总之,自从它们有了样式规则,我就需要在IDE中设置它:不然我会收到任何关于non-final的警告。样式规则也可以用于找出可能的bug,比如比较自动装箱对象,确保引用的一致性,又或者,使用==会参考自动装箱的引用对象原始值。

不幸的是,现代IDE不再需要我们花精力去学习怎样使用它们。我在Unix平台完成第一个C程序时,不得不花点时间学习如何使用vi编辑器,因为它的学习曲线很陡。几年之后来看,这些时间花得很值。我甚至是用vi来起草的这篇文章。现代IDE学习曲线非常平滑,结果就是我们对其背后大量工具的基本用法上,再也得不到进步。

我学习IDE的首要任务是记住快捷键。当我手指放在键盘上敲代码时,用鼠标切换导航菜单时会中断敲代码,所以我会按下Ctrl+Shift+I来内联变量防止动作流被打断。这些中断会导致不必要的上下文切换,如果每件事都用这种笨办法,那会让我生产力非常低。同样的规则也可以用于键盘技能:学习快捷键模式,你不会后悔这次早期投资。

最后,作为一个程序员,我们拥有久经考验的Unix工具,来帮助我们操作代码。例如在代码审查期间,我要关注一个程序员是否有大量相同的类命名,我可以通过find、sed、sort、uniq以及grep等工具非常简单地实现,就像这样:

1
find . -name "*.java" | sed 's/.*\///' | sort | uniq -c | grep -v "^ *1 " | sort -r 

我们请一个管道工到家里来,是因为我们需要的是它的喷灯。所以让我们花点时间学习如何更高效地使用我们的IDE。

秦汉历时410年,之后便进入了长达400年的大分裂时期,分别是三国、西东两晋、五胡十六国、南北朝。这是怎样一个混乱的年代,而这样一个乱世又演绎了多少成王败寇,为后世留下了多少华夏精神?貌似除了三国,我们对这段历史知之甚少,大概因为太混乱了,都没人想提起。这个时期上演了无数次兵戎相见、尔虞我诈、谋权篡位、倒行逆施、改革换血,那主旋律是什么?没有,或者说混乱与迷茫本身就是这个时期的标签。还好,当新的中华文明迎来破晓,方知这是一场伟大的洗礼,是中国历史的必经之路。

不一样的三国

大概每个中国人都知道三国,但绝大部分人耳熟能详的应该是罗贯中的《三国演义》。很可惜,三国演义不过是民间文学,源于历史而高于历史。按照易中天在书中的观点,三国在整个中国历史上根本不重要,它不过是东汉末年由于王朝衰败而导致的军阀割据,只是一个顺理成章的步骤,很多朝代更迭时都会经历的短暂时期。

此外,我们津津乐道的草船借箭、温酒斩华雄、桃园三结义、关羽华容道放走曹操、诸葛亮空城计骗司马父子等等。都是虚构的!事实上,曹操是宦官养子,孙策是寒门子弟,刘备是没落贵族之后。三人的开局都不怎么样。而各种士人将军也没有那么神乎其神。赵云后期被刘备冷落无所作为,诸葛亮治国有方但军事上没有太多业绩,而关羽的各种奇迹完全是后人吹出来的,刘禅也并非窝囊废。还有曹操,少年放荡不羁,曾和袁绍一起去偷别人家的新娘子……

其实历史是这样子的,汉朝由吕后(刘邦媳妇)开辟了一种传统嘛,皇帝掌权—>太后掌权—>宦官掌权—>又回到皇帝手里,宫廷内的权力争夺就这么循环着。于是东汉末年,何太后掌权,但并不打算对宦官赶尽杀绝,于是大将军何进就把董卓拉拢过来,准备威胁太后。结果请神容易送神难,董卓一来就只手遮天,胡作非为,皇帝是废了又立立了又废,从此朝廷暗无天日。

于是大豪族袁绍出场,利用自家的名望号召全国各大门阀,准备团战讨伐董卓。结果一呼百应,各大军事力量集结,那场面之吓人。话说曹操也跟来了,军队区区五千人,在联军当中连个座次都没有。但真的讨伐董卓时,唯有曹军奋勇厮杀,却是孤军奋战,联军们在干嘛?纸上谈兵,夜夜笙歌。一心想要复兴汉室的曹孟德明白不能与这帮乌合之众为伍。他准备自己单干,招兵买马,攻下兖州,有了自己的地盘,开始屯田治理,做大做强。

董卓死了,吕布杀的。但各方势力继续窝里斗,皇帝却因此而颠沛流离。于是毛玠给曹操提了个建议:奉天子以令不臣(也就是后来所说的挟天子以令诸侯)。即打着皇帝的名义,以合法的方式讨伐那些不听话的集团势力。就这样,曹操手握天子逐步拿下长江以北,最终改国号为“魏”,其子曹丕更是逼献帝禅让,直接称帝。曹魏一度雄霸北方,算是三国在军事上最成功的。

刘备呢,一个被遗忘的皇室之后,常年寄人篱下,后来军败曹操,不得不投奔荆州刘表。荆州可是战略要地,而刘表晚年病重时将荆州托付于刘备(此段历史存疑),他算是捡到大便宜了。而诸葛亮与孙吴的鲁肃都看到汉室已衰,三分天下是大势所趋,二人几乎想到了一起,于是孙刘联盟,共御曹敌。此后在刘备称帝,改名蜀汉,在诸葛亮的儒法并用的管理下,这个国家内政最稳。但连年征战却无功而返,并不得民心。

孙吴是典型的墙头草,因为他们执政者根本不在意复兴汉室,他们只在乎活下去,然后一统天下。所以可以看到孙家一会联刘,一会媚曹。说白了都是权宜之计,政治需要罢了。但孙吴的内政不稳,窝里斗很严重,吴国之所以最后灭亡,完全得益于他们见风使舵的外交政策。

总之,三国的故事每个人都耳熟能详,是真是假我就不好多言。我觉得大概记住几场战役:

  • 官渡之战:袁绍伐曹,曹操大胜,从此确定曹魏的北方霸主地位。
  • 赤壁之战:曹操伐刘,刘备联合孙吴来了个火烧连营,从此刘备稳坐荆州,三足鼎立之势凸显。
  • 夷陵之战:刘备伐吴,一是为关羽报仇,二是抢回荆州,结果孙权大胜。正式开启三分天下模式。

晋,是福是祸?

晋似乎不被认作一个朝代,司马家族从曹操时代一路辅佐曹家一统天下,先是干掉最弱的蜀汉,逼得刘禅在洛阳装疯卖傻乐不思蜀才能苟且偷生;而后收复孙吴。待帝国的版图基本构建完成时,又效仿当年曹丕,逼迫魏元帝曹奂禅让(不知道曹操的棺材板还压得住不),改国号为“晋”。

就我个人觉得,司马家族本可以成为中国历史上一个响亮的朝代,中国历史也会因为他们而改写。因为晋完成了统一大业,剩下的就是安抚贵族,与民休息。但他们偏偏开了历史的倒车,居然效仿周人,发扬封建主义。周朝验证了长期封建制会带来凌驾于天子的诸侯,西楚霸王验证了短期封建制会内乱不止。而司马家族偏不信邪,搞什么以郡建国,果然,短短几十年的稳定后引来了八王之乱。

其次,晋的世族制,实际上就是贵族体系,高官世袭,致使那些官二代们毫无上进心,整日高谈阔论,甚至发展为酗酒、嗑药、娘炮文化。这样的人群管理国家,能走向富强么?

封建招致国家内忧外患,世袭搞的政治风气败坏,也才能有后来的五胡乱华。而南北朝时期,自诩为华夏正宗的汉人把南方搞的是乌烟瘴气,而北方胡人却真正继承了华夏的衣钵。

这些是晋之过,却并非历史之错,毕竟这个时期为中华文明留下了遗产。西晋好比春秋,东晋好比战国。前者给了中华思想一次井喷的机会,正如春秋的诸子百家;后者给中国的政治体制来了一次全面反思,正如战国的秦并天下。

先说思想,魏晋时期,人们不在关注儒家的繁文缛节,而更在意一个人的气节,这个人是否获得漂亮与洒脱。这当然都拜世袭所赐,大家的前途都被一眼看到底,就不在务实了,尽搞那些虚的东西。所以这个时期的世族阶级喜欢高谈阔论,出现了玄学(中国的早起哲学)、琴棋书画盛行,而道家思想更是衍生出了道教,其实就是诸子百家的思想大杂烩。如果想要理解那个时期的晋人,不妨看看日本,因为那个时期中国的木屐和汉子都传入了日本,很多人文思想也连带着过去。总之那个时期的人,讲究事物背后的神形意,喜欢优柔唯美的风景,却又看中个人的率真与敞亮气质。

新战国时代

再谈谈体制,从周开始,中国有皇室、诸侯、贵族、百姓等阶层,有阶级就有矛盾,于是秦汉废除了这些阶层,从此只有皇权和臣民。此外,中国四周是蛮夷戎狄,即汉人与少数民族的矛盾,之前采取的措施一直是御敌,然后采取通婚来换取和平。

从封建制走向郡县制,是为了消除阶级矛盾,消除内战。毫无疑问,秦汉做得非常好,而且经过400多年的和平稳定,汉文明已经彻底融入中国人的骨髓里。阶级矛盾解决了,那么,民族矛盾呢?

少数民族是向往中华文明的,这与西方历史很不一样,古罗马时代,外来入侵者只会攻城略地,根本不在乎罗马文明,他们要重建自己的文化。而华夏的天下观却感染了外族势力,他们和汉人在思想上是一致的,即“我是否有资格继承天子这一角色”。所以,西晋时期各种权利争夺上演了八王之乱,进而招致北方各游牧民族趁乱入侵,逐一建立了各种外族政权,史称五胡乱华。但他们打的旗号都是:“我才是华夏正宗”!

五胡:匈奴、鲜卑、羯、羌、氐。由于文化的落后,基本还停留在部落联盟时代,最高领袖基本就是个大酋长。所谓五胡乱华,其本意就是指外来落后的野蛮文明扰乱中华文明。另外,所谓十六国则是由八王之乱后的内部分裂的新政权。因此这个时期中国的情况是,南有汉人内讧,北有外族乱政,各种分裂与吞并,大大小小何止十六个国家。说它是新战国时代,一点也不夸张。

五胡乱华,是晋的混乱与腐败给了少数民族一次说话的机会,让他们登上历史舞台,与汉人一起思考,何谓华夏文明。不过,这个时期的胡人与汉人一样,打打杀杀,你方唱罢我登场,局面一直持续到鲜卑族。其大酋长拓跋珪甚至先进文明有多重要,因此任用贤能,励精图治,与民休息。待盘踞北方称帝后,他废除了部落酋长制度,改为臣民百姓,迁都平成,让鲜卑一步跨入帝国大门,国号“魏”,史称“北魏”。

此后,拓跋家族的后代逐步汉化,直到孝文帝拓跋宏时,他的言行几乎与汉人无异,而他自己也认为天明在即,也许有了一统天下的野心。于是拓跋宏先是迁都洛阳,而后改用汉族祭祀礼仪、改汉姓、禁胡语、禁胡服、加强胡汉通婚。易中天的点评很合意:鲜卑像盐一样把自己溶化在汉民族的水中。

南北朝,即以长江淮河为界,以北的拓跋家族统治政权,称为北朝;以南的汉族政权,称为南朝。这个概念对后世的影响极大,因为此前中国人的观念里,只有中原没有南北,因为东南西北都是野蛮文明。此后中国人眼里,没有中原却有了南北方的概念,比如北方豪爽,南方温和等。

说会南朝,这些自诩正宗的汉人还在忙于内乱,还没争出个老大。因为封建制的诸侯依然顽固,世袭制养的窝囊废还在腐败。这便是南朝之乱。

历史车轮滚滚向前,直到北方的鲜卑人汉化的够彻底,汉人也足够认同鲜卑文化,中华文明才真正迎来的破晓时分。因为鲜卑化的汉人杨坚登场了,我们都知道他便是之后的隋炀帝。北朝探索了民族融合的道路,南朝验证了阶级矛盾的根源,经过这样的洗礼,中华文明才探索出了一条同时搞定民族与阶级矛盾的新方向。而这一些便以隋朝拉开帷幕,稳定持续了唐宋元明清五朝,一千多年。

Know Well More than Two Programming Languages

编程人员心理上都达成一个共识很长时间了,那就是编程专业技能的高低,与程序员所知道的不同编程范例的数量直接相关。这里说的知道不是简单了解,或者略懂略懂,而是能真正的编程。

每个程序员开始采用一门语言编程后。这门语言就会影响程序员对软件的思考方式。如果他们仅停留在这门语言,不管有多少年的编程经验,他们都将只会这门语言。一个只会一门语言的程序员,他的思维将被这门语言彻底限制住。

程序员学习第二门语言将会是一种挑战,尤其是当这门语言的计算模型与之前的大相径庭。C、Pascal、Fortran,都有相似的计算模型。选择从Fortran转到C带来的挑战并不会太多。从C或Fortran转到C++或Ada所带来的挑战主要是编程思想。从C++转到Haskell是一次很有意义的改变,当然其挑战也很有意义。从C转到Prolog将会带来非常明确的挑战。

我们可以列举一些计算机编程范式:面向过程、面向对象、函数式、逻辑式、数据流,等等。在两个不同范式间切换,就会产生最大的挑战。

为何这些挑战是好的呢?这与我们思考对算法实现、风格、适合的实现模式相关。其中,交叉受益是专业的核心。适用于一门语言的解决问题的风格,或许拿另一门语言就行不通了。尝试不同的风格,从一门语言到另一门语言,才能真正教会我们这两种语言,以及问题是如何被解决的。

采用不同的编程语言交叉受益将会带来巨大的影响。或许最为明显的就是采用命令式语言实现的系统中增加了或正在增加很多表达式声明。任何精通函数式编程的人都能轻松地适应声明式,即便是使用C这样的语言也如此。采用声明式方案一般会让程序更短或更易于理解。例如C++,几乎是为了表达式声明,肯定会全力支持泛型编程。

说了这么多,每个程序员都应该掌握至少两种不同的编程范式,理想情况下应该掌握上述五种。程序员始终应该对学习新语言感兴趣,最好从陌生的范式学习。即使他们的日常工作中使用相同的编程语言,当一个人能够从其他范式中交叉受益时,为这门语言带来的进步也是不容小觑的。雇主应该考虑到这点,并为员工学习当前未使用的语言留出培训预算空间,作为进步的一种方式,该语言要使用起来。

即便已经开始了,一个礼拜的培训课程是不足以学习一门新语言的:它通常需要几个月的实践,哪怕是兼职,也是为了获得一门语言真正意义上的工作知识。方言的使用,不仅仅是语法和计算模型,这一点很重要。