面向硬件模块的跨平台开发环境搭建

中山野鬼 发布于 2016/02/16 14:01
阅读 944
收藏 6
忘了是年前还是大年初一,写了一篇《面向硬件系统设计的工程组织方式》水帖,其实是个大体想法。过年七天,外加一天,总算把这个想法的整体框架折腾完了。也并了几个模块进去。这里新开一个帖子,讨论一下具体的问题,思路和方式方法。如果有好的批评意见还望多给指导。哈。

挖坑篇
反正折腾事,总要有理由。所以先谈为啥挖这个坑。
先从一句理想主义的老话说起“一次编写,到处编译”。如果真信这8个字,你就等着被坑吧。哈。
不过从开发角度,尽可能少的做无用功,尽可能少的给自己加bug ,还是有必要追求的。这里要先给“开发”加个定语。想来想去,比较准确的描述是“面向硬件模块”。
再准确点是,“针对自控系统,在多平台设计之上的面向硬件模块”的开发工作。
这个和纯软编程是有些区别的。
纯软编程,独立的代码模块具有完整独立的价值,通过操作系统,底层库,编译工具等,使得他们和硬件几乎隔离(这里的硬件包含主cpu以及外部设备),或者说代码的价值和硬件关联不大,提换底层平台实现跨平台很容易,而且该做的工作是操作系统,底层库和编译工具的事情。
而一套自控系统,真正有价值的是用了什么执行部件,采集了什么感知数据,进行了如何的数据分析和反馈控制,可以说整体系统,是各个外部设备之间的有机关联,其包含了设备之间的互通(通过mcu),设备之间数据的互作用(通过mcu之上的计算策略与方法),相反主控mcu并不是重点。简单说,围绕业务的,是基于mcu之上执行的与外部设备对应的整体系统逻辑。再说人话,值钱的是以外围设备作为“重要组成”的系统,在数据采集方式,数据分析方法,设备驱动方式方法等方面的内容。至于在什么特定mcu上实现,并不是这类系统的必要条件,mcu厂家不用瞎bb,换你比换衣服还快,是我的追求,相反,外部设备模块,不行了才换,毕竟都是一家人了(系统组成)。
因此同样是代码,一旦脱离了外部设备,这些mcu上跑的code ,就毫无疑义了。所以区别“纯软”设计的模块概念,这里给出个新名词,面向硬件的模块。其包含两个部分。一个部分是在mcu上跑的驱动代码,一部分是面向外部设备特性的代码,差异很简单,前者由mcu决定,后者基于外部设备特性和通信驱动规则决定。

如果我们只是纯粹看代码,那么驱动代码可分为,面向mcu的代码逻辑和面向外部设备的代码逻辑。前者和mcu特性有关的,后者和外部设备的驱动方式(协议/模式)有关的。再细分第一个部分可分为 与mcu内在特性有关的;外部设备与mcu引脚连接方式有关的,两子类。如下图。

虽然代码通过mcu和外部设备交互,但实际上mcu仅仅是个执行者,在这方面,此时的mcu面向自控系统而言,沦落为一个底层驱动,实现软件逻辑和外部设备的融合。

这里使用了ex_dev control & data manage,是对外部(相对mcu)设备驱动控制或感知数据采集,及相关数据分析处理的统称。

重谈理想主义的8个字,“一次编写,到处编译”。落在下面的几个场景,显然是不可能的。
1、换腿了,简单说,通过不同的mcu引脚连接外部设备。极端情况,你需要将替换前后的引脚配置代码重新写一遍。
2、换通信的实现方式了。例如,一开始用模拟spi,后面改用mcu内部的硬件spi。
3、换片子了,换一个系列的片子还好,从avr的8位mcu换到stm32,你和我说不改代码?你当你是java啊。

其实针对一个面向硬件的模块设计,上面的场景还不足以让我挖个坑然后自己填了8天(春节吃的好,总是困,如果开挂的工作模式,可能5天就搞定了)。

看下下面的设备。

两种mcu系列。atmega328p 和stm32f103。
四种型号,
atmega328p-au ,atmega328p-pu,差异是后者是pdip封装,只有28个腿,少了ADC6/7两个腿。
stm32f103c8t6 ,stm32f103rct6,外部引脚的差异(内部存储空间不谈了。。)前者48个腿,后者64个腿,前者自然少了很多东西,例如前者只有3个usart,两个spi,后者有对应有5个和3个。
6种板级方案
其中atmega328p-au有三种,这里是开源硬件的,arduino-uno,arduino-mini-pro ,arduino-nano 。此处就不去数他们的引脚数量和位置的差异了。

简单说,一个外部设备,用不同mcu系列时,得面对的是整个编译、链接和基础库等工具链的开发环境的差异。

用一个系列的不同型号时,得考虑不同的MCU硬件资源。

用一个系列同样型号,不同的板级方案时,还要考虑不同引脚的分配方案。

这些差异,在实际开发中,我遇到以下几种情况:
1、今天用arduino-uno(atmega328p),做了外部设备的驱动验证,明天得把它挪到stm32上。如果你对这两个片子足够了解就知道,mcu内部硬件单元的驱动,avr系列比stm32简单很多,后者还有片内总线,下面部分代码的例子可以看出来,哈。一个引脚折腾,你除了配置gpio还要使能总线、时钟,如果有中断响应时,还要配置中断优先级。对于外部设备,正常的开发套路,肯定是测试环境少挖坑,先保证设备驱动成功,然后再考虑其他,所以有上述两个替换动作。
2、今天用这个mcu硬件单元(如spi1),明天用另一个(spi2),别问我为什么,各种情况,甚至对应腿烧掉。哈。特别是整体系统综合设计时,还得兼顾电路设计方面的情况做选择。
3、今天为了测试这个设备,对mcu硬件单元这样配置,明天为了测试另一个设备,对mcu硬件单元那样配置。两次都ok,但回到从前,因为某个地方忘了改回去,凭空多了个人为bug,看着设备上电,然后就没有然后了。于是p4的追版本。。。

简单看下面的图,打XX的表示你不得不重新修改代码的内容。


整个code中,最值钱的是不打xx的,最不值钱的是那些打xx的,但是不同于“纯软”代码,不打xx的,没有活的外部设备,几乎没有任何意义。

为了解决上述开发中的问题,也尽可能的复用已有代码,降低人为bug,保留最重要的内容不变。年前纠结了一个星期后,终于决定,立贴挖坑,于是就有开篇所说的那个帖子。

传统(问题)篇
这里先说下传统方式。谈不上是特流行的方式,也可以说,挖坑前我的开发环境。
参见下面两个目录。

/avr
	/apps
		/BLDC_chk
		/GamePad
	/hardware
		/cores
		/libraries
			/nrf24l
			/inv_mpu
			/BLDC
			...
	/include
	/lib
	/tools
		/bootloaders
		/avr
			/avr
			/bin
			/etc
			...


/stm32
	/apps
		/BLDC_chk
		...
	/hardware
		/cores
		/libraries
			/nrf24l
			/inv_mpu
			/BLDC
			...
	/include
	/lib
	/tools
		linker.ld
		/gcc-arm-none-eabi-4_9-2014q4
		...

这里有avr和stm32两目录,/avr /stm32 简称相对根目录后面直接用/表示。每个目录下面分了/apps, /hardware , /include ,/lib ,/tools

如果是针对多个外部设备联动构成的工程,则存储在apps下,这部分相对比较值钱。
在hardware下存在 cores和libraries 两个子目录。
/hardware/cores就是针对该系列mcu的基础驱动代码。相对“纯软代码”设计,这算是真正的底层驱动代码。
/hardware/libraries,下存储的就是很值钱,很值钱的,独立面向外部各种硬件设备的模块代码。我并不认为它是驱动模块,如果从软件系统层级分,应该算中间件甚至是引擎级别的,例如自控平衡算法,是放在/hardware/libraries中的,毕竟和特定外部感知设备关联。
/include 是所有需要给其他工程模块引用的头文件。对应的库文件则存储在lib下。
/tools的标准配置,就包含了一些makefile里引用的配置文件,以及厂家提供的编译、链接和基础库等工具。

原先这么构造开发工作目录时,曾经为了一件事情纠结了半天(确实半天,大概10个多小时,从到公司,到下班回家,路上才决定不纠结)。
“对mcu各个硬件单元的配置信息应该放哪”,这是个坑,如果从坑的级别上说,属于那种经常绊倒人的小阴沟,虽然摔的疼但很快能爬出来,同时总觉得填了它成本过大的,真正的阴坑级。
“mcu的各个硬件单元的配置信息”,是指,例如,这个spi要这样用,这几个gpio要定义成output,那几个gpio要定义成input,等等。
原先本着咱做系统产品的,咱牛掰的心态(成套系统中,各个外部设备与mcu的关联会逐步固化),于是就把配置信息,存放在cores/inc/xx_hwcfg.h 和cores/inc/xx_hwdef.h中。(其实真正的牛,是走得慢,走得稳的。给点流动空气就上天的,那是“猪”。)
两者的区别是,xx_hwcfg.h是从模块角度来定义,xx_hwdef.h是从mcu的硬件单元角度来定义,例如如下的代码,
...
#define USED_1K_MISSION 1
#define USED_DMA_TRANS 0
#define USED_MPU6050 0
#define USED_NRF24L01 1
#define USED_BLDC 0
#define USED_SPI_FLASH 1
...
#if (USED_SPI_FLASH == 1)
#define W25X_FLASH_SPI SPI3
#undef USED_SPI3
#define USED_SPI3 1
#define SPI3_NSS_GPIO GPIOA
#define SPI3_PIN_NSS (GPIO_Pin_15)


#define SPI3_SPEED SPI_BaudRatePrescaler_256 
#define SPI3_DIR_MODE SPI_Direction_2Lines_FullDuplex
#define SPI3_DATASIZE SPI_DataSize_8b 
#define SPI3_CPOL_MODE SPI_CPOL_High
#define SPI3_CPHA_MODE SPI_CPHA_2Edge
#endif
以上是xx_hwcfg.h的代码摘抄,上面用于定义不同外部模块对应代码是否打开,根据不同模块与mcu的硬件单元的关联关系,设置对应mcu的硬件单元的使能宏。

以下是xx_hwdef.h的代码摘抄:

#if (USED_I2C2 == 1)
#undef GPIOB_PIN10
#undef GPIOB_PIN11


#define GPIOB_PIN10 ((GPIO_Mode_AF_OD) | (GPIO_Speed_50MHz << GPIO_SPEED_SHF))
#define GPIOB_PIN11 ((GPIO_Mode_AF_OD) | (GPIO_Speed_50MHz << GPIO_SPEED_SHF))
...
#endif
这里是针对与外部模块设定没有特定关联,仅针对mcu内部各个硬件单元的定义,特别是mcu内部的关联定义。
这样做的好处是,将#define USED_SPI_FLASH 1 修改为0,则对应SPI3就不会被驱动。当然还有其他好处。反正一开始用的还挺爽。直到各种硬件模块越来越多时,当时纠结的事情就发生了。
例如,系统中,需要用3个spi,一个给nrf24l01,一个给驱动led的6803芯片,一个给w25x系列的spi flash。正好那几天手上的stm32f103rct6烧了(搞电机时,一股青烟),活还得干,测试w25x的flash驱动,不得不使用用stm32f103c8t6。这货没spi3啊,于是转用spi1口。对应上述xx_hwcfg.h改改,其实xx_hwdef.h中也需要改改,测试通过。又过一天,折腾回 /libraries/nrf24l01的模块,结果罢工。p4追回,隔两天,又追p4,把flash再次做验证。

更不爽的是,在avr/libaries/下有nrf24l01的代码,在stm32/libiarais下,也有nrf24l01的代码。为了保证逻辑对齐,但凡有改动,就得两个目录merge。代码文本对齐是小事,如果涉及到针对mcu本身的设置代码,还得跑到另一个mcu系列的板子,做对应修改,并做验证。原本无刷电机的模块,bldc,仅仅是针对stm32,但因为要用atmega328p做个小测试板(前面帖子上过图),结果又是一通代码移植,新增pwm的驱动,atmega328p的电机驱动一直用量化bit的方式,不是pwm的方式,但外部无刷电机指定要pwm,也不能不给啊。

这就是跨平台带来的不爽。(java兄,来来来,你不号称一次编写到处运行嘛,帮忙解决一下,别跑呀

面向对象篇
还是那句老话,面向对象是种思考方法,和啥语法没毛线关系。和语法有关系的是基于面向对象的编程语言。这里只是使用面向对象的思考方法对前面的问题给出分析,并得出一套方案。当然我希望有更好的方案建议。

先类比一下。在自控系统中,面向硬件的模块所对应的代码,可以看作纯软设计中,c语言里库,c++里的类,各种更高级语言里的包,统一概括就是系统中的组成模块。这些都是纯软设计中,有价值的业务模块,可以换硬件,升级操作系统或底层库,这些模块不会有什么改动。
不过很悲催的是,从前面“传统问题篇”介绍的代码组织方式,面向硬件模块的代码实现,等同于(纯软设计中)一个包类库里定义了指定的cpu,指定的操作系统等的特性内容。mcu厂家嘿嘿一笑,恨不得让你所有的业务逻辑都关联到它所出品的系列mcu中的特性。我还是那个态度,我只伺候客户的体验度,轮不到我伺候你mcu厂家的方案,别让我对mcu不爽,我不爽,就随时让你滚蛋。
所以我们需要分层。把mcu的特性隔离出来。以方便想让某个mcu的方案滚蛋就滚蛋。哈。
整体代码中,可以粗分三层。如下图左上。
在下图右上两个图中,我们实际有两个外部设备模块,使用相同mcu,但使用了不同的连接配置方式,此时对应的代码就会有差异。
更别提我们更换mcu,类似的板级组织方案,如左下两个代码方案,也是要改代码的。而右下的图与下中的图差异在于使用了不同的外部设备。


而如果上述三个外部设备模块ext_devX1 X2 X3的逻辑部分都设计好了。按理说是不用变的。不过看一下下面的代码摘抄:

#define SPI_Flash_Write_Enable() do{W25X_FLASH_CS_LOW();\
SPI_Flash_SendByte(W25X_CMD_WriteEnable);\
W25X_FLASH_CS_HIGH();}while (0)
#define SPI_Flash_Write_Disable() do{W25X_FLASH_CS_LOW();\
SPI_Flash_SendByte(W25X_CMD_WriteDisable);\
W25X_FLASH_CS_HIGH();}while (0)



spi flash的写使能和关闭。其都包含三个动作,针对两个mcu硬件端口单元。 ce引脚和spi口。上述代码的逻辑,属于外部设备的逻辑,而具体代码的实现,既包含了板级组织方案的内容(W25X_FLASH_CS这个引脚具体用哪个GPIO口),也包含了具体mcu硬件单元的设置。即便你固定使用某个spi,但stm32的spi 操作接口和avr的 spi操作接口函数还是有些区别的。

简单讲人话,针对外部设备的处理逻辑中,内嵌了mcu和板级方案的特性。因此我们需要
面向mcu的硬件单元;
面向系统板的引脚组织方式;
面向外部设备的驱动方式,
来区分代码内容,分别处理。也即面向三种对象,面向mcu,面向系统板,面向外部设备。
这里需要解释下,面向mcu和面向系统板的区别。
这两个层面的工作,其实都是对mcu的具体硬件单元的配置和具体通信接口的操作(我们把引脚拉高拉低,抽象的看作一种通信方式),此处简称操作。但所有针对mcu的配置或操作,如上面所讨论的,存在两种情况。与外部板级系统连接方式“有关的”和“无关的”。对于后者,例如,我的整体系列设计中,针对一个具体的mcu而言,无论系统板的引脚组织方式怎么改变,内部始终需要一个定时器,用来做微秒级的计数,还需要另一个时间中断,做毫秒级实时任务栈的调度(就是一堆外部设备的执行代码的入口函数,到了指定毫秒,调它一次)。而一个系统板的方案,对nrf24l01是使用spi1,而另一个方案是使用spi2,根据不同的系统方案,mcu内部存在差异的配置方式,甚至是一个模拟口,由此,将这类和系统板方案有关联的配置工作,称为面向系统板的内容。

面向外部设备的,就更好理解了。无论使用什么mcu,无论是通过mcu的硬件单元,还是通过gpio模拟出的通信协议,代码中都存在不变的内容,这些内容是针对具体外部设备的(抽象意义上的)通信,包括有组织的发送或采集数据,有针对性的关联或分析数据等。这些实现逻辑,其实是整体系统设计中很值钱,很值钱的。。


分了层,就得谈统一(函数)接口的问题。针对一类mcu,针对mcu硬件单元的配置操作,还好统一,针对不同mcu,接口都统一了,底层代码就折腾了。因此,我们增加一个虚拟层。
虚拟层的作用是尽可能的不让面向外部设备的逻辑代码,受到底层配置的影响。于是我们就存在4层方式。看一下下面的图就明确了。其一样可以形成4种具体设计。


增加一个virual_mcu目的是让外部设备模块中和具体mcu配置与操作进行隔离。外部设备模块仅针对virual_mcu进行配置和操作,而virual_mcu在编译时,选择性的使用不同的库和板级配置方案,再将那些mcu配置和操作动作,进行具体化。


这样,当我们在某一个测试系统中,对一个外部设备模块进行验证测试后,就可以将其与mcu有关的操作,使用virual_mcu层面的内容来定义。而对应其他mcu或板级方案,只需要具体针对mcu的配置工作进行设计,或构建新的板级配置方案。如果这个mcu不是新来的,其实mcu cfg部分,也即最下面蓝色方框内的内容,是无需再设计的。从开发工作内容来看,则是面向mcu的开发,面向板级方案的(配置)开发,面向具体外部设备模块的逻辑开发,也即面向(差异硬件)对象的开发。

由此最大化复用已有代码设计,降低人为bug。

当然具体实现,我仍然习惯用c语言,因为有强大无比的宏,实在不行上代码自动生成哦,再不行,拿出大杀器lex&yacc,哈。这里引出“面向对象”,无非是通过面向三类硬件对象的分析,引出系统分层组织方案而已。

工具与makefile篇

题目说的是“开发环境的搭建”。开发环境,我的理解在广义上,包括两方面,一大类是工程目录组织方式,一大类是工具链和底层库。好吧,IDE和文本编辑器+脚本没啥区别,其实就是个壳子而已,这里也权当一种工具吧。
由于面对不同mcu。我们得组织不同工具,由此,本帖算是讨论开发环境的搭建。
当然我希望有大一统的gcc家族,啥都支持。不过很可惜,暂时没找到。反正avr系列和stm32系列,大多数工具和底层库都是使用官方的。由于是在mac os X 下做开发,所以针对stm32的链接配置文件甚至是一条条抄出来的。简单说,目前我手上,没有大一统的工具链和底层库,没办法有组织有纪律的面向不同mcu系列构建单一的工具链。(当然谁有,希望能介绍一下)。因此在开发环境的根目录,下文简称”相对根目录"下,存在一个tools目录。其下目前存在三个目录。avr,bin,stm32。

/tools
	/avr
		/avr
		/bin
		/etc
		...
	/stm32
		linker.ld
		/bin
		...
	/bin
atmel官方针对avr的开发工具和库,都丢到avr目录下,各种渠道收集到的stm32的开发工具和库丢在stm32下,包括抄出来的链接配置文件。bin下则放了一堆脚本和模版文件,以及针对x86_64环境下的相关配置文件。毕竟有些代码,会在电脑上跑跑,做模拟测试。

工具归归类后,就需要开始折腾makefile。
以下是makefile的标准模版。
sinclude $(AUTO_EM_TOOLS)/bin/board.cfg

MODULE_NAME = ~_DST_~
TEST_FILE=test_~_DST_~_main
SRC_FILE= 
ASRC_FILE=
CPPSRC_FILE=
EXTERNAL_LIB_NAME=
BOARD_PATH=
sinclude $(AUTO_EM_TOOLS)/bin/project_directory.cfg
ifneq (BOARD_PATH,)
BOARD_INC=$(addprefix -I$(AUTO_EM_INC)/,$(BOARD_PATH))
FLAG_INC += $(BOARD_INC)
BOARD_LIB=$(addprefix $(AUTO_EM_LIB)/,$(BOARD_PATH))
TAIL_INFO=waring!!!! \*\*\*\* this make used $(BOARD_PATH)/board_cfg module \*\*\*\*
endif
ifeq ($(MCU_SERIES),SIMU)
sinclude $(AUTO_EM_TOOLS)/bin/simu_toolchain.cfg
else ifeq ($(MCU_SERIES),AVR)
sinclude $(AUTO_EM_TOOLS)/avr/toolchain.cfg
else ifeq ($(MCU_SERIES),STM32)
sinclude $(AUTO_EM_TOOLS)/stm32/toolchain.cfg
endif
ifneq (BOARD_PATH,)
LDFLAGS += -L$(BOARD_LIB) -lboard_cfg
endif
$(DEPEND_FILE):$(ALLSRC_NAME)
	$(RM) $@
	$(CC) -MM $(CFLAGS) $(CFLAGS_EXT)  $(FLAG_INC) $^ > $@
	sed -i '' '/.o:/ s,^,$(OBJ_PATH)/,' $@

sinclude $(DEPEND_FILE)

$(OBJ_PATH)/%.s.o:$(ASM_PATH)/%.S $(SELF_FILE)
	$(CS) $(SFLAGS) $(SFLAGS_EXT) $(FLAG_INC) $< -o $@

	
$(OBJ_PATH)/%.o:$(SRC_PATH)/%.c $(SELF_FILE)
	$(CC) $(CFLAGS) $(CFLAGS_EXT)  $(FLAG_INC) $< -o $@
	
$(LIB_NAME):$(OBJFILE) 
ifeq ($(SRC_FILE)$(ASRC_FILE)$(CPPSRC_FILE),) 
	@echo $^
else
	$(AR) $(ARFLAGS) $(ARFLAGS_EXT) $@ $^
endif	

clean: 
	$(RM) $(CLEAN_OBJ) 
	$(RM) $(CLEAN_BIN) 

rebuild:clean build 

.PHONY: build clean rebuild 

这里 引用了4个文件。除了$(DEPEND_FILE)这个依赖文件外,包括一开始引用的board.cfg,随后引用的project_directory.cfg,和之后的toolchain.cfg。(~_DST_~)可以无视,仅仅是我自己的模版替换工具,将~_DST_~内容,替换成指定的模块名称,由此形成一个有效的makefile而已。

TAIL_INFO这个变量很好使,真的很好使,其使用位置在
build:xxx
     @echo $(TAIL_INFO)
其目的是在编译/链接完成后,最后给出一个提示,当前工程编译使用了哪个板级方案。自己被自己坑过,所以特加上这个变量。

board.cfg,里面是用来对与系统板上和外部设备无关的硬件配置变量(针对make的变量,此处区分针对c代码的宏)进行设置。目前比较简单,根据make后的给入变量 MCU_TYPE的值,定义出MCU_SEIRES的内容。毕竟avr和stm32的工具链并不仅仅针对atmega328p和stm32f103系列。例如下面的一些内容:

ifndef MCU_TYPE
$(error need define MCU_TYPE, such as "make MCU_TYPE=atmega328p"  "MCU_TYPE=stm32103rct6"...)
endif

ifeq ($(MCU_TYPE),atmega328p)
MCU_SERIES = AVR
else ifeq ($(MCU_TYPE),atmega168)
MCU_SERIES = AVR
else ifeq ($(MCU_TYPE),stm32f103rct6)
MCU_SERIES = STM32
else
MCU_SERIES = SIMU
endif

在project_directory.cfg中仅仅是定义一堆和路径相关变量。例如下面一些内容:

ifeq ($(SRC_PATH),)
SRC_PATH=src
INC_PATH=inc
OBJ_PATH=obj
BIN_PATH=bin
LIB_PATH=$(BIN_PATH)
INCLUDE_PATH=$(INC_PATH)
INCLUDE_PATH+=$(AUTO_EM_INC)
INCLUDE_PATH+=$(AUTO_EM_INC)/$(MCU_TYPE)

EXTERNAL_LIB_PATH= $(AUTO_EM_LIB)


MSRC_NAME= $(addsuffix .c,$(addprefix $(SRC_PATH)/,$(TEST_FILE)))
ASRC_NAME= $(addsuffix .S,$(addprefix $(ASRC_PATH)/,$(ASRC_FILE)))
SRC_NAME= $(addsuffix .c,$(addprefix $(SRC_PATH)/,$(SRC_FILE)))
CPPSRC_NAME= $(addsuffix .cpp,$(addprefix $(SRC_PATH)/,$(CPPSRC_FILE)))

上述两个文件定义的内容,和具体使用哪种工具链无关,所以统一放在/tools/bin下。与工具相关的,toolchain.cfg则分别存放在/tools/avr 和 /tools/stm32下。

他们对相同的变量有不同的赋值。例如下面两组内容:
CMD_PATH=$(AUTO_EM_TOOLS)/avr/bin
CC = $(CMD_PATH)/avr-gcc
此存储在/tools/avr/toolchain.cfg 中。
CMD_PATH=$(AUTO_EM_TOOLS)/stm32/bin
TC=$(CMD_PATH)/arm-none-eabi-gc
此存储在/tools/stm32/toolchain.cfg中。
$(AUTO_EM_TOOLS)是环境变量,指向 相对根目录下的 tools目录。
由于原先stm32和avr的makefile中,build目标的依赖不同,因此懒一把,将这些差异的目标与依赖,放在不同的toolchain.cfg中。所以你在makefile中看不到诸如这些内容:
build: $(EEP_NAME) $(HEX_NAME) 
	@echo $(TAIL_INFO)

$(EEP_NAME):$(BIN_NAME)
	$(OBJCOPY) $(OJBFLAGS) $(OJBFLAGS_EXT) $< $@
$(HEX_NAME):$(BIN_NAME)
	$(HEX) $(HEXFLAGS) $(HEXFLAGS_EXT) $< $@ 

$(BIN_NAME):$(MOBJ_NAME) $(LIB_NAME)  
	$(BIN) $(BIN_FLAGS) $(BINFLAGS_EXT)  -o $@ $<  $(LDFLAGS)

以上是tools/avr/toolchain.cfg中的内容。

就上述这些工具的相关文档的组织和makefile的构造,折腾我两天。而且 make 的命令变复杂了。正常需要这样:
make MCU_TYPE=atmega328p rebuild
或者
make MCU_TYPE=stm32f103rct6 rebuild
于是,多出两个脚本,smake.sh ,amake.sh 存放在 /tools/bin下。里面的内容很简单。这里给出smake.sh的完整内容。
#! /bin/sh
make MCU_TYPE=stm32f103rct6 $@

如果有什么更好的工具及makefile的组织方案,还望给出指正意见。不过要注意,这不是简单的交叉编译,上面针对不同mcu系列(必要下,后期会根据新的mcu系列进行扩展,我们毕竟随时保持这替换mcu方案的态度,哈)且要能在mac os X下工作,大一统的gcc-xxx是否存在,存在了是否真心好使,这就不知道了。

当然,系统库更新的脚本(用于根据依赖规则,将各个模块,依次全部重新编译,并将头文件和库文件复制或合并到相对根目录下的include 和lib中的脚本),也得有些调整和扩展,以方便针对不同的工具链编译和打包库文件。
折腾好这些,就可以开始组织代码了。

代码组织篇
虽然是面向硬件设备的,不过首先要组织的是一堆基础定义。包括类型结构,基础操作,如 
#define _abs(a) ((a) < 0) ? (0 - (a)) : (a)
#define _min(a,b) (((a) < (b)) ? (a):(b))
和一些面向特定数据结构的算法。例如目前我们开发中串行数据传递都会使用一个打包模块,简单说,就是将多个不同数据类型的多个数据打成一个可以指定长度的包进行传递。无论是串口通信还是nrf24l01的无线通信都用它。

上述这些代码,被独立组织,存放在相对根目录的type_ds下。旗下实际包括 /common_def /datastruct /types三个二级目录。这些二级目录下,才有各种工程目录。例如:

/type_ds
	/common_def
		/gs_defnes
	/datastruct 
		/gs_SPacket
	/types
		/gs_types

type_ds下的内容,基本是不变的。因此在系统库更新时,统一塞到库 xxx_base.a中。实际文件目前就有三个。一看就懂。 

libatmega328p_base.a 
libstm32f103rct6_base.a
libsimu_base.a

最后一个是,是直接可在mac os X下调用运行的库。此时的基础库基本上和mcu是无关的(后面会把特定mcu的库,合并塞到上述特定的.a中),但编译器不同,编译目标不同,因此分了三个.a文件。所以,从此大多数时间就是一套代码,一个makefile ,多套库。

有了基础库,就要开始折腾面向硬件对象的设计了。这些都集中在相对根目录的 hardware目录下。分别是
/hardware
	/mcu
	/board
	/virual_mcu
	/ext_device
这和年前写的帖子有出入。当然如果有更好的组织方式,也希望给予指正。
/mcu下,将存储面向mcu的代码工程。
/board下,存储面向系统配置方案的代码工程。
/virual_mcu,虚拟mcu模块,其实就是一个工程,真正有价值的,被更新的,是他的头文件。

先看mcu。其下目前有两个目录:

/mcu
	/atmega328p
	/stm32f103rct6

其实就是传统版本中,avr/hardware/cores和stm32/hardware/cores中的部分内容。少了,和系统板关联的配置代码。

/mcu下的代码,在做系统更新时,为了不添乱,不折腾什么同名冲突问题,我选择了最老实的方式。将
/mcu/atmega328p/inc下的头文件更新到 /include/atmega328p下。

对应生成的库文件,则直接追加到libatmega_base.a的基础库文件中。

/mcu下的工程是没有必要三种编译的(avr,stm32,x86),针对atmega328p的mcu配置工程,你用stm32的编译工具玩一把,这是闹哪样?

在/mcu/xxx/inc目录下,有两个头文件是值得商榷的。也希望提出指正意见。
一个是mcu_define.h,一个是mcu_cfg.h。
mcu_define.h是对mcu配置模式类别的一些定义,例如如下内容:
#define SPI_UNDEF 0
#define SPI_M_3LINE 1 //SCK MOSI MISO MASTER MODE
#define SPI_S_3LINE 2 //SCK MOSI MISO SLAVER MODE
#define SPI_M_2LINE 3 //SCK MOSI
#define SPI_S_2LINE 4 //SCK MOSI
#define SPI_SIMU_3LINE 5
#define SPI_SIMU_2LINE 6
这里表示了7中spi的工作类别模式(不是具体的工作模式)。如未定义(不使用),3线主方式,3线从方式,2线主、从方式,还有模拟方式。一个mcu来了,查查手册,根据对引脚可能产生的差异,大体分分类,在这个头文件里定义定义,就不会改了。
而mcu_cfg.h中就比较复杂了。
以stm32的版本为例,则先有诸如下面的内容:
#ifndef USED_SPI_1
#define USED_SPI_1 SPI_UNDEF
#endif
#ifndef USED_SPI_2
#define USED_SPI_2 SPI_UNDEF
#endif
#ifndef USED_SPI_3
#define USED_SPI_3 SPI_UNDEF
#endif
意思是,如果没有定义这些宏,则将其设定为不使用。
随后有
#if ((USED_SPI_1 == SPI_M_3LINE) || (USED_SPI_1 == SPI_S_3LINE))
#ifdef GPIOA_PIN5
#error SPI_1 3 lines need  GPIOA_PIN5
#endif
#ifdef GPIOA_PIN6
#error SPI_1 3 lines need  GPIOA_PIN6
#endif
#ifdef GPIOA_PIN7
#error SPI_1 3 lines need  GPIOA_PIN7
#endif
#define GPIOA_PIN5 ((GPIO_MODE_AF_PP) | (GPIO_SPEED_50MHz << GPIO_SPEED_SHF))
#define GPIOA_PIN6 ((GPIO_MODE_IPU) | (GPIO_SPEED_50MHz << GPIO_SPEED_SHF))
#define GPIOA_PIN7 ((GPIO_MODE_AF_PP) | (GPIO_SPEED_50MHz << GPIO_SPEED_SHF))
#elif ((USED_SPI_1 == SPI_M_2LINE) || (USED_SPI_1 == SPI_S_2LINE))


#ifdef GPIOA_PIN5
#error SPI_1 2 lines need  GPIOA_PIN5
#endif
#ifdef GPIOA_PIN7
#error SPI_1 2 lines need  GPIOA_PIN7
#endif
#define GPIOA_PIN5 ((GPIO_MODE_AF_PP) | (GPIO_SPEED_50MHz << GPIO_SPEED_SHF))
#define GPIOA_PIN7 ((GPIO_MODE_AF_PP) | (GPIO_SPEED_50MHz << GPIO_SPEED_SHF))
#elif ((USED_SPI_1 == SPI_SIMU_3LINE) ||(USED_SPI_1 == SPI_SIMU_2LINE))
 	#error SPI1 not support simu in this version
#endif
意思是,如果3线方式,而且GPIOA_PIN5, GPIOA_PIN6,GPIOA_PIN7 被定义过,那么编译error。其实重定义,编译器是会提示的,但是仅是warning,一不小心没看到,就这么过去了。
随后,是针对硬件spi的方式,将对应的gpio口进行定义,当然这里偷懒,spi1的模拟驱动没写,索性直接#error 。
之后,会有诸如这样的代码:
#ifndef GPIOA_PIN13
#define GPIOA_PIN13 GPIO_UNDEF
#endif
#ifndef GPIOA_PIN14
#define GPIOA_PIN14 GPIO_UNDEF
#endif
#ifndef GPIOA_PIN15
#define GPIOA_PIN15 GPIO_UNDEF
#endif
这是补齐对GPIO的模式定义。GPIO的一个引脚需要使用,对应端口就需要使能,于是关联定义又跑出来多组,其中一组是:
#if (\
(GPIOA_PIN0 != GPIO_UNDEF)||\
(GPIOA_PIN1 != GPIO_UNDEF)||\
(GPIOA_PIN2 != GPIO_UNDEF)||\
(GPIOA_PIN3 != GPIO_UNDEF)||\
(GPIOA_PIN4 != GPIO_UNDEF)||\
(GPIOA_PIN5 != GPIO_UNDEF)||\
(GPIOA_PIN6 != GPIO_UNDEF)||\
(GPIOA_PIN7 != GPIO_UNDEF)||\
(GPIOA_PIN8 != GPIO_UNDEF)||\
(GPIOA_PIN9 != GPIO_UNDEF)||\
(GPIOA_PIN10 != GPIO_UNDEF)||\
(GPIOA_PIN11 != GPIO_UNDEF)||\
(GPIOA_PIN12 != GPIO_UNDEF)||\
(GPIOA_PIN13 != GPIO_UNDEF)||\
(GPIOA_PIN14 != GPIO_UNDEF)||\
(GPIOA_PIN15 != GPIO_UNDEF)||\
(0))
#define USED_GPIOA 1
#else
#define USED_GPIOA 0
#endif
还好写了个小c程序,自动生成上面的代码,不然敲的累死是无所谓,敲错就是真活该了。这就是传说中的生不如死。
余下是对应初始化的部分定义,例如:
#define GPIO_INITD() do{\
GPIO_Cfg(48+0,GPIOD_PIN0);\
GPIO_Cfg(48+1,GPIOD_PIN1);\
GPIO_Cfg(48+2,GPIOD_PIN2);\
}while (0)
#define GPIO_INIT() do {GPIO_INITA();GPIO_INITB();GPIO_INITC();GPIO_INITD();}while (0)

#if ((USED_SPI_1 != SPI_UNDEF) && (USED_SPI_1 !=SPI_SIMU_3LINE) && (USED_SPI_1 !=SPI_SIMU_2LINE))
#define SPI_1_INIT() do{SPI_Config(SPI1,SPI1_SPEED,SPI1_DIR_MODE,SPI1_DATASIZE,SPI1_CPOL_MODE,SPI1_CPHA_MODE,SPI_FirstBit_MSB);}while (0)
#else
#define SPI_1_INIT() do{}while (0)
#endif
#define SPI_INIT() do{SPI_1_INIT();SPI_2_INIT();SPI_3_INIT();}while(0)
上面仅仅给了针对GPIOD端口的定义,另外三组没给出。此处用了两种代码组织方式。前者是参数选型判断,后者是模式选择性判断。差别在于,前者在执行中通过代码对GPIOx_PINy的定义值判断完成。后者在预编译时,通过宏判断完成代码选择。
关联定义结束啦?没呢,这就是我说为啥喜欢新设备来了,先用avr玩的原因。
#if ((USED_SPI_1 != SPI_UNDEF) &&\
(USED_SPI_1 != SPI_SIMU_3LINE) &&\
(USED_SPI_1 != SPI_SIMU_2LINE))
#define USED_SPI1 1
#else
#define USED_SPI1 0
#endif
为了简化配置代码中的模式判断,这里将USED_SPI_1的模式情况转变到USED_SPI1宏中。为啥,看看下面的代码:
#if (USED_SPI2 == 1)
  APB1_EN |= RCC_APB1Periph_SPI2;
#endif
#if (USED_SPI3 == 1)
  APB1_EN |= RCC_APB1Periph_SPI3;
#endif
 ...
 RCC_APB1PeriphClockCmd(APB1_EN,ENABLE);
#if (USED_SPI1 == 1)
  APB2_EN |= RCC_APB2Periph_SPI1;
#endif
...
RCC_APB2PeriphClockCmd(APB2_EN,ENABLE);

这些是总线对相关mcu硬件单元的使能。大体这么描述吧,虽然不严谨。

当然,另一种代码组织方式,每配置一个硬件单元,例如spi1,就把其关联的gpio口,总线资源等放在一个函数中定义。这是好方法,不过仅仅是针对一个硬件单元。以我摔坑的经验,如果你要对很多硬件单元进行配置,甚至有可能有潜在的分配冲突,那么还是分类统一配置的方式更好些。
上述内容都好了,问题就来了。一堆堆宏的定义和判断,那么起头的坑爹宏如:
USED_SPI_1
在哪定义的?
当然是在/board/stm32f103rct6/下的某个工程里。
简单说,对与系统板关联的mcu的定义和配置,实际存放在面向系统板的对应工程中。
所有的针对mcu的配置定义代码也被分了两大类。在mcu/stm32f103rct6下的配置可以看默认配置,例如如下的内容:
void RCC_def_Init(void){
	uint32_t APB1_EN = 0;
	uint32_t APB2_EN = RCC_APB2Periph_AFIO;
	PCC_CLK_set();
	APB2_EN |= RCC_APB2Periph_GPIOA;

	APB2_EN |= RCC_APB2Periph_TIM1;
	APB2_EN |= RCC_APB2Periph_USART1;
	RCC_APB2PeriphClockCmd(APB2_EN,ENABLE);
	APB1_EN |= RCC_APB1Periph_TIM2;
	RCC_APB1PeriphClockCmd(APB1_EN,ENABLE);
}
void GPIO_def_Init(void){//for usart1
	
	GPIO_Cfg(9,GPIOA_PIN9);
	GPIO_Cfg(10,GPIOA_PIN10);
}

因为我的习惯,总是默认保留一个串口用于测试。所以无论系统板怎么变来变去,只要是stm32系列,usart1永远都是具备一个功能:和pc或其他监测系统通信。所以在/mcu/stm32f103rct6的 stm32_def_Init函数下,会固定调用上述两个函数。

相对应的,在/board/stm32f103rct6/下的所有工程里,都存在这样一个函数,而且一模一样:
void core_init(void){
  SystemInit();
  stm32_def_Init();
  AHB_APB_RCC_Init();
  
  set_USART_DBG(USART_CH1,1);
  
  ...
  stm32_nvic_Init();
  Serial_Str("SystemInit...!");
  ...
  set_USART_DBG(USART_CH1,0);
  timer_onMillis_En(1);
  return ;
}
表示是对mcu内核配置的初始化函数。看不到宏?没事,给出一个局部函数的部分代码:
static void AHB_APB_RCC_Init(void){
  	uint32_t APB2_EN = 0;
  	uint32_t APB1_EN = 0;
  	uint32_t AHB_EN = 0;
  	RCC_def_Init();
#if (USED_GPIOA == 1)
  	APB2_EN |= RCC_APB2Periph_GPIOA;
#endif
#if (USED_GPIOB == 1)
  	APB2_EN |= RCC_APB2Periph_GPIOB;
#endif
  ...
#if (USED_SPI1 == 1)
  	APB2_EN |= RCC_APB2Periph_SPI1;
#endif
 ...
  	RCC_APB2PeriphClockCmd(APB2_EN,ENABLE);
 ...
#if (USED_TIM3 == 1)
  	APB1_EN |= RCC_APB1Periph_TIM3;
#endif
#if (USED_TIM4 == 1)
  	APB1_EN |= RCC_APB1Periph_TIM4;
#endif
...
#if (USED_SPI3 == 1)
  	APB1_EN |= RCC_APB1Periph_SPI3;
#endif
#if (USED_I2C1 == 1)
  	APB1_EN |= RCC_APB1Periph_I2C1;
#endif
#if (USED_I2C2 == 1)
  	APB1_EN |= RCC_APB1Periph_I2C2;
#endif
...
  	RCC_APB1PeriphClockCmd(APB1_EN,ENABLE);

#if (USED_DMA1 == 1)
  	AHB_EN |= RCC_AHBPeriph_DMA1;
#endif
  
  	AHB_EN |= RCC_AHBPeriph_CRC;
  	RCC_AHBPeriphClockCmd(AHB_EN, ENABLE);
}
简单说,即便是同一款mcu,针对不同系统板,(哪怕还是使用同一个电路板,只要引脚的使用用途发生改变,杜邦线一改,则认为是一个新的系统板)也对应新的工程。不过新的工程也好,旧的也罢,里的唯一的c代码文件,src/board_cfg.c(主测试c代码,包含main函数的,不会丢入库中)是一模一样的。
差异则全部在board_cfg.h里。这里给出其中一个board_cfg.h的部分代码
#ifndef _BOARD_CFG_H
#define _BOARD_CFG_H
//ins_inc_file
#include "gs_types.h"
#include "gs_defines.h"
//ins_typedef
//********* logic port define 
#define nrf24l01_CE_PORT GPIO_A
#define nrf24l01_CE_PIN GPIO_8

#define nrf24l01_NSS_PORT GPIO_A
#define nrf24l01_NSS_PIN GPIO_4

#define nrf24l01_SPI SPI_1

#define MPU6050_I2CX I2C1
#define MPU6050_I2C I2C_1
#define MPU6050_ADDRESS I2C1_DEV_ADDRESS
...

//********** mcu unit define SPI ***************
#define USED_SPI_1 SPI_M_3LINE
#define SPI1_SPEED SPI_BaudRatePrescaler_8 //9MHz
#define SPI1_DIR_MODE SPI_Direction_2Lines_FullDuplex
#define SPI1_DATASIZE SPI_DataSize_8b 
#define SPI1_CPOL_MODE SPI_CPOL_Low
#define SPI1_CPHA_MODE SPI_CPHA_1Edge
....
//************** mcu init define I2C **************
#define USED_I2C_1 1
#define I2C1_SPEED 400000
#define I2C1_DEV_ADDRESS 0x55
#define I2C1_EV_IRQ I2C1_EV_IRQn
#define I2C1_EV_IRQ_PPrior 1
#define I2C1_EV_IRQ_SPrior 0
#define I2C1_ER_IRQ I2C1_ER_IRQn
#define I2C1_ER_IRQ_PPrior 1
#define I2C1_ER_IRQ_SPrior 0
...

而在board_cfg.c中,存在这样的引用。

#include "board_cfg.h"
#include "mcu_cfg.h"
此处,mcu_cfg.h实际引用的是 /include/stm32f103rct6/mcu_cfg.h。makefile文件中,会根据MCU_TYPE做路径选择。

不同的板级系统配置方案对应的工程,虽然两不同,但代码组织上工作量不大。因为c代码完全一样,直接用脚本构造工程时把代码都折腾好,根据实际情况调整board_cfg.h就可以了

为了方便面向设备的代码工程引用,上述各种配置方案对应的工程存在同名的board_cfg.h和libboard_cfg.a。因此这里用了最简单的方法。针对头文件则更新到

/include/board/atmega328p/xx目录下。

或者 /include/board/stm32f103rct6/xx目录下。

库文件libboard_cfg.a则存放在诸如

/lib/board/atmega328p/xx
/lib/board/stm32f103rct6/xx

的不同目录下。在针对某个设备模块的工程编译时,选择不同板级配置,是通过修正 makefile里的 BOARD_PATH变量的内容来实现。此时改makefile一处,就可以完成不同配置方案的调整(mcu或系统板)这样的方式比代码里到处修改要方便的多。最关键是少出bug。

折腾完配置,就存在对mcu硬件单元在操作方面的映射处理(分层构造目的是隔离,实际实现是映射)。例如传统的代码有这样的内容,此是nrf24l01模块内部的代码:
#ifndef STM32F103
#define CE_LOW() do{Port_outpin(SPI_PORT,SPI_CE_PIN,0);}while (0)
#define CE_HIGH() do{Port_outpin(SPI_PORT,SPI_CE_PIN,1);}while(0)
...
#define SPI_RW(data) SPI_transfer(data)
#else
#define CE_LOW() do{GPIO_SetBits(NRF24L01_CE_GPIO,NRF24L01_CE_PIN);}while (0)
#define CE_HIGH() do{GPIO_ResetBits(NRF24L01_CE_GPIO,NRF24L01_CE_PIN);}while (0)
#define SPI_RW(data) SPI_transfer(NRF24L01_SPI,data)
#endif
此处#ifndef STM32F103表示针对atmega328p的。
调用函数不同,接口参数也不同。于是我们就需要整合。将上述宏统一对应到virual_mcu上也即虚拟的mcu。
虚拟mcu的模块很简单。就是一个工程,virual_mcu。甚至没有有价值的c代码。有价值的是存放一堆#define映射内容的头文件。
virual_mcu有两类头文件。一类是virual_define.h。用于定义各种虚拟的端口号此处就不贴代码了,和任何mcu无关,和任何外部设备无关,和你创建什么规模的虚拟mcu有关。
另一类是针对各种虚拟端口操作,面向不同mcu的操作映射定义。
首先是virual_cfg.h,其内容如下:
#ifndef _VIRUAL_CFG_H
#define _VIRUAL_CFG_H
#define STM32F103RCT6 1
#define ATMEGA328P 0
#include "gs_types.h"
#include "gs_defines.h"
#include "virual_define.h"
//ins_def
#if (MCU_TYPE==ATMEGA328P)
#include "virual_atmega328p.h"
#elif (MCU_TYPE == STM32F103RCT6)
#include "virual_stm32f103rct6.h"
#endif
#endif /* _VIRUAL_CFG_H */
这里等于是分类引用,通过编译命令中跟随的宏参数来选择。对应贴上述两个引用文件的一些定义:

在virual_atmega328p.h中有:

#define GPIO_A_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_B_OUT(pin,val) do{Port_outpin(PORTB,pin,val);}while (0)
#define GPIO_C_OUT(pin,val) do{Port_outpin(PORTC,pin,val);}while (0)
#define GPIO_D_OUT(pin,val) do{Port_outpin(PORTD,pin,val);}while (0)
#define GPIO_E_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_F_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_I_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_G_OUT(pin,val) do{UNDEF_OP();}while (0)



在virual_stm32f103rct6.h中有:
#define GPIO_A_OUT(pin,val) do{if (val) GPIO_SETS(GPIOA,_BV(pin));else GPIO_RESETS(GPIOA,_BV(pin));}while (0)
#define GPIO_B_OUT(pin,val) do{if (val) GPIO_SETS(GPIOB,_BV(pin));else GPIO_RESETS(GPIOB,_BV(pin));}while (0)
#define GPIO_C_OUT(pin,val) do{if (val) GPIO_SETS(GPIOC,_BV(pin));else GPIO_RESETS(GPIOC,_BV(pin));}while (0)
#define GPIO_D_OUT(pin,val) do{if (val) GPIO_SETS(GPIOD,_BV(pin));else GPIO_RESETS(GPIOD,_BV(pin));}while (0)
#define GPIO_E_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_F_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_I_OUT(pin,val) do{UNDEF_OP();}while (0)
#define GPIO_G_OUT(pin,val) do{UNDEF_OP();}while (0)
反正虚拟的逻辑端口,有很多很多,真正能用的,我们给定义;不能用的,就给空。这倒不怕出错。因为明明是针对atmega328p的系统板配置,压根没A端口,你却非要如下写:
#define nrf24l01_CE_PORT GPIO_A
#define nrf24l01_CE_PIN GPIO_8
这样跑出来,出错了。我只能说你自己挖坑望里跳,还盖个盖子不让人捞你出来。
这里就不得不再次讨论一下board中的board_cfg.h。重复部分代码,这是针对stm32f103rct6的某个板级方案,如下:
//********* logic port define 
#define nrf24l01_CE_PORT GPIO_A
#define nrf24l01_CE_PIN GPIO_8

#define nrf24l01_NSS_PORT GPIO_A
#define nrf24l01_NSS_PIN GPIO_4

#define nrf24l01_SPI SPI_1
...

//********** mcu unit define SPI ***************
#define USED_SPI_1 SPI_M_3LINE
#define SPI1_SPEED SPI_BaudRatePrescaler_8 //SPI_BaudRatePrescaler_8 //9MHz
#define SPI1_DIR_MODE SPI_Direction_2Lines_FullDuplex
#define SPI1_DATASIZE SPI_DataSize_8b 
#define SPI1_CPOL_MODE SPI_CPOL_Low
#define SPI1_CPHA_MODE SPI_CPHA_1Edge



这里有两组定义。上面是针对各种外部设备模块,与虚拟mcu硬件单元或引脚对应的定义(映射),如nrf24l01_CE_PORT等。这里定义的GPIO_A,GPIO_8都是逻辑端口。这些定义和mcu的配置工作没有半毛钱关系。而下面才是和具体的板级配置有关的定义。
也即,一个特定的系统板级方案,其配置定义头文件, board_cfg.h既服务于mcu的配置,也服务于外部设备的虚拟定义。实话实说,这种组织方式,我纠结了2天。原本想用两组头文件分别定义,但考虑到统一配置,降低bug的原则,还是搁一块了。
有了这些配置,那么我们面向设备的模块就可以大统一了。当然需要独立出来一个模块的配置头文件。如下是nrf24l01_cfg.h,被实际的nrf24l01.h所引用:
#include "board_cfg.h"
#include "virual_cfg.h"
//ins_typedef
//ins_def
#ifndef nrf24l01_CE_PORT
#error need define nrf24l01_CE_PORT in board_cfg.h
#endif
#ifndef nrf24l01_NSS_PORT
#error need define nrf24l01_NSS_PORT in board_cfg.h
#endif
#if (nrf24l01_CE_PORT == GPIO_A)
#define nrf24l01_CE_HIGH() GPIO_A_OUT(nrf24l01_CE_PIN,HIGH)
#define nrf24l01_CE_LOW() GPIO_A_OUT(nrf24l01_CE_PIN,LOW)
#elif (nrf24l01_CE_PORT == GPIO_B)

#define nrf24l01_CE_HIGH() GPIO_B_OUT(nrf24l01_CE_PIN,HIGH)
#define nrf24l01_CE_LOW() GPIO_B_OUT(nrf24l01_CE_PIN,LOW)
#elif (nrf24l01_CE_PORT == GPIO_C)
#define nrf24l01_CE_HIGH() GPIO_C_OUT(nrf24l01_CE_PIN,HIGH)
#define nrf24l01_CE_LOW() GPIO_C_OUT(nrf24l01_CE_PIN,LOW)
...
#else
#define nrf24l01_CE_HIGH() do{}while(0)
#define nrf24l01_CE_LOW() do{}while(0)
#endif
以上内容仅仅是针对 nrf24l01_CE口的拉低,拉高。虽然是一堆定义,还好,自动生成,不伤键盘。
这样nrf24l01的模块就和具体的mcu没关系了。要调整,除了换板子,换插线方式外,只需更换 makefile中BOARD_PATH的定义,好让编译器找到指定的board_cfg.h,被nrf24l01_cfg.h所引用即可。而不用改动nrf24l01模块中的任何代码。

到此,整个面向硬件模块的开发环境讨论完毕,最后给出一个关联依赖图。

好吧,我承认,通过一个图,描述所有关联,失败了。先上一张库的关联图。

如上图描述,与mcu无关的,最基础的各种工程,makefile存在多样性设置,分别构建不同的基础库。对应面向mcu的工程,通过指定的mcu_type的内容,打包到特定的基础库中。

 上图给出的是一个下载到mcu的bin文件,在连接时与不同层级库的选择方式。对于board solution这个层级,则需要依据MCU_TYPE 、BOARD_PATH,两个 make的变量来选择,其他情况仅需要前者。

开发步骤

最后,这里说一下对于不同的开发任务的开发步骤。

如果是新来一个mcu。则先在mcu cfg这个层级进行设计。最简单的方式时将未来应该存放在/hardware/board/xx/board_cfg.c 里的 core_init();及对应的局部函数,在main.c文件中实现。并在main.c中做对应测试。

随后,进行分离设计。将main.c中的core_init();及调用的局部函数,放回到一个新的板级配置方案的代码工程boardX中。而测试函数在boardX的工程中的main.c中实现。

接下来,就是在virual_mcu工程中,增加对应的映射关系。主要是创建一个新的virual_xx.h。并被virual_mcu.h引用。当然,对应makefile和所引用的一些配置文件需要增添新的内容。

最后,就是将已经有的外部设备模块的代码,通过修改MCU_TYPE和BOARD_PATH,实现与基于新的mcu构造的板级配置方案的关联。因为已经处理完毕的外部设备模块,对mcu的资源调用被virual_mcu隔离,所以无需对外部设备模块的代码工程进行任何调整。

如果是新来了一个设备,需要构建新的外部设备模块。

首先,建议用单一的硬件平台对应的测试工程,而不是本文的大一统的方式进行进行测试。如果avr 8位单片机能对通的,就不要用stm32。先保证设备“点亮”。简单说,能激活,能有基本的交互。

随后,核对其所需要的通信端口的特点,根据系统设计文档,在该设备代码工程,假设为 deviceX中,增加deviceX_cfg.h。对对应虚拟端口进行定义。

接着是针对测试硬件平台中mcu的类型,和连接方式,构造一个板级方案的代码工程。并进行测试。

最后,才是针对其他类型的mcu,构造对应的板级方案的代码工程,进行跨平台的测试。


加载中
0
叫我刀刀
叫我刀刀
肯定有人问,图是什么软件画的
0
中山野鬼
中山野鬼

引用来自“叫我刀刀”的评论

肯定有人问,图是什么软件画的
哈,omnigraffle 。
0
乌龟壳
乌龟壳

一句话来说,代码组织根据业务情况,灵活运用宏、函数等工具解耦就可以了。

此篇说那么多其实主要在说面向硬件系统的业务是怎么回事。

0
中山野鬼
中山野鬼

引用来自“乌龟壳”的评论

一句话来说,代码组织根据业务情况,灵活运用宏、函数等工具解耦就可以了。

此篇说那么多其实主要在说面向硬件系统的业务是怎么回事。

没更新完呢。包括如何实现的。算是抛砖引玉。快速解决业务实现任务是宗旨,怎么快,怎么不出错怎么来。所以如果有更好的工具组织方案能提出来,我也好再进步一下。哈。
中山野鬼
中山野鬼
回复 @乌龟壳 : 现在还不敢完全推倒重来,目前做好的这个跨平台开发环境,其实大部分是以前多个工具链的整合。哈。
乌龟壳
乌龟壳
回复 @中山野鬼 : 你如果要java的interface技术优化代码,就用宏来做接口呗。
中山野鬼
中山野鬼
回复 @乌龟壳 : 没有最方便,只有更方便。采用c++的类封装我也考虑过。不考虑效率和代码量问题,针对单一mcu是ok的,但是针对多样性mcu,也是要一堆折腾的。。
乌龟壳
乌龟壳
我不熟悉这方面的东西,没啥好建议的咯,C你都那么熟了,还怕啥不知道咋个封装法么~~
0
k
kchr
多少年了,还在玩 Hello World 级别的东西。
0
redblueme
redblueme
搬好小板凳看鬼哥,我给鬼哥的专业精神打82分,剩下的我以666的形式送给你,不怕你骄傲。
0
仓禾
我对鬼爷一向是尊重的,但是文章里提到的东西,乌龟壳的话说是:“其实主要在说面向硬件系统的业务是怎么回事”,我的感觉就是:问题是老问题,嵌入式大部分人都会遇到这问题,解决方案也是老方案,而且也没有什么特别好的新方案。
代码组织,大都是SDK抽象功能,宏来控制,makefile来选择,#if、##、函数指针等用的帅一点,适应性更强些。操作系统(也可能没有,只是自己实现了个死循环)适配了通用外围设备。
如果SDK有不同,或者操作系统有不同,那就再加一层adapter,把应用和底层功能再分开一下,把操作系统里面的线程进程(有些操作系统里概念不一样)再抽象一下。
如果系统要适配不同的产品,那就把功能再模块化一下,模块之间的通信可以用API,也可以试试用MQ等实现的Bus,也可以直接用Socket,特别是设备之间需要分布式调用时,加一层Bus更容易实现些。
Java也就是封装了操作系统操作,加了些基础lib,只不过是操作系统之间的基本理论趋同,而mcu以及外围设备千差万别。
但是,话说了这么多,道理大家都懂,但是实现……每个都麻烦的,而且稳定性才是最重要的,抽象、分层都是慢慢来的。
我在的公司主要卖嵌入式设备,销售额也有一百多个亿了,但是代码质量还是挺惨的,任重而道远。
返回顶部
顶部