7815

帖子

56

TA的资源

裸片初长成(中级)

21
 
当然,LS这段代码,不用人写,是自动产生的。
这也可以让我们联想到 我最早接触的 是 Cypress PSOC 4那个图形化配置工具。

到后来,我大意法STM32,也有了的 Cube X.
虽然形式不太一样,但大抵都差不多的效果。

所以也许你会认为我大惊小怪,反正复杂的是自动生成的,关我啥事,我只在意我看到的那个勾选就搞定很简单的东西。

话是不错。
然而,很悲催的事实是。

这些看起来很复杂的配置代码,不是没事拿来给你看的,因为最终产生在代码里,做驱动的,正是这些代码。

比如刚刚那个GPIO的IO 0

PS:
我那时候的项目,公司有底层库,所以我在代码里也没找到几个地方。
所以我直接截图,总共就三句直接调用了。
虽然我当时偷懒,直接写数字,懒得写那些宏,但我相信,你们一定可以明白,传递到这个 gpio_cfg()里的那复杂的4,5个参数,实际上都对应着上面的这些配置里的什么东西。




对于特别熟悉STM32的人,不用我废话都早就明白了。
个人签名

强者为尊,弱者,死无葬身之地


回复

7815

帖子

56

TA的资源

裸片初长成(中级)

22
 
在这个帖子里,我尽量从简。
但我还是大费周章,把这个其实很简单的配置文件,发了一通。
我虽然不是有心恶心你们,但我的确是故意为之。

对,我就是要让你看到,这种配置的可怕之处。

int gpio_cfg(GPIOF, PIN11, GPIO_MODE_OUT, GPIO_SPEED_50MHZ, GPIO_MODE_PULL_OUT, GPIO_MODE_PUPD);
......

只不过是一个GPIO口的配置,假如,这是在BSP里——不管是一个编译成lib,.a之类的或者是一个一旦写完,基本整个项目期,你都不再管的xx.c的话,那么,我没意见。

但是很多时候,我们经常在代码里看到的不是这样子。

这个初始化用的 gpio_cfg还稍微好点,毕竟通常就在初始化函数里,只有一次。
如果是像这种

gpio_set(GPIOA,PIN3,RESET);
gpio_set(GPIOA,PIN3,SET);
gpio_get(GPIOA,PIN3);
.......

我就问你烦不烦?
烦,也许不烦,因为有万能的 ctrl+c ctrl+v

那么好了,凡是可以用 粘贴复制的事情,我现在都能忍。
但是,有的时候,但遇到移植的时候,事情就没这么好办了。
 
个人签名

强者为尊,弱者,死无葬身之地

 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

23
 
刚刚这个,是我15年的时候,在STM32F407上的一个项目。
我先不纠结 后来的 HAL库什么的,是不是换了另一种gpio口的声明接口。

我就假设,换了一种单片机,我以我仅有不多熟悉的51为例。

大家都知道,到了51这里,什么GPIA GPIOB都不见了。
它直接是 P01 = 1; P02 = 0;

这时你会想,嗨,那有什么?
我还是 ctrl+c ctrl+v呗

是的,你还是可以这样做,但是,不好意思。
为了替换同样一个管脚,比如接 LCD的RST脚的 PA3(GPIOA PIN3),你就要在所有用到它的地方修改一次。

RST还算一个用得少的语句,万一是一个 直接操作LED的GPIO口,而且这家伙根本就不屑封装一个 LED_ON(),LED_ON()
那,那你就真的倒霉催了。
你可能不止在一个 .c里改。

你可能会整个项目代码 搜索,最后发现 超过50处,需要把

gpio_set(GPIOA,PIN3,RESET);

改成

P01 = 0;

然后还有 50多处

gpio_set(GPIOA,PIN3,SET);

要改成

P01 = 1;

写到这里,你还是不服。
不不不,我不是这样做的。

我可是注意可移植性的,我是这么做的。

#define LCD_RST_LOW  gpio_set(GPIOA,PIN3,RESET)

所以,我只需要在一个地方,改成

#define LCD_RST_LOW  (P01 = 0)

的确,一点没错。
这样做就很好了。

然而,吹毛求疵一点,我就想问你
既然你都 #define LCD_RST了,为什么,不干脆

void lcd_rst()呢?
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

24
 
在这里,其实从减少修改的角度来说,两者基本没差别。
但是,事实上,它的的确确有一个小差别。

那就是,宏是发生在预编译阶段。而函数则不然。

举个例子。

如果现在,你基于同一个板子,写两个工程。
由于板子是一摸一样,其实你的这些 IO脚配置也就一模一样。

如果你的代码里是
#define LCD_RST的话。那么,事实上,你每次编译代码,(假设你是 Rebuild all的话),那么,你其实,每次都在把 LCD_RST这句代码重新编译一次。

而如果你用函数,既然是完全一模一样的板子配置,你完全可以直接把这个 void lcd_rst()函数 (当然也包含其他)直接一次编译成一个库。
好处就是,下一次你直接调用这个库。

在功能上,和你直接调用这个函数没有一丝一毫差别。
但是,你却再也不用重新编译一次 void lcd_rst()这个函数的实现本身。

=================

当然,后面我会说到,之所以,我不惜换一个MCU,乃至换一个板子,我宁可重写一次 BSP的实现。
是因为我认为在一个正式的项目里,这部分的代码占比都非常低,具体的百分比我不好说,但你的程序功能越复杂,这部分的比例就会越小,绝大多数时候可能根本不会超过5%(从代码行数来看)。

那么,为什么我还要在这里强调这个问题呢?
我承认,这有点吹毛求疵,但并非完全无意义。

第一,我也说过。
对于单片机程序而言,其实 所谓 底层驱动 和 上层应用,在代码规模和复杂度上拉不开距离。
所以,我们对于底层驱动的封装逻辑,同样可以应用到上层应用上。

所以,我不仅会在 板子不变的情况下,直接把BSP编译成一个库,我还会把其他基本不变的代码,编译成库。

比如一个以字符char为元素的 队列实现。

因为我知道它们(至少在这个项目上)根本不需要改变,所以我不想每次都重新编译它们,我也不想每次检视代码,或者说,不想把它们挂在心上。

因此——结论就是:
一旦完成就基本全开发过程不需要改动的东西,从一开始,就可以做成一个库,把所有底层相关,特殊的东西封进那个普适应的函数名字里去。

void lcd_rst_low(void);

至于下面是 P01 = 0;

还是 gpio_set(GPIOA,PIN3,0);

我才懒得管。

============ 重要的分割线 ==============

一句 P01 = 0;或者复杂点,一句 gpio_set(GPIOA,PIN3,RESET);
就封进一个函数?
你一定会觉得我大动干戈。

然而别忘了,我只是以最简单的GPIO为例。

想象一下,如果现在,我是要做I2C的开始时序,它当然涉及一个 CLK脚和SDA脚。
尽管,I2C的START时序很简单,但——是否已经足够给你一个封装进函数里的理由?

还是说,你特别喜欢,在 i2c的 write_byte,read_byte里都copy一次?
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

25
 
PS:
一开始我是很想,先仔细打草稿,修改完,再发上来的。
但实际做的时候,我还是发现,先写出来,再修改可能更实际。
所以写到具体内容的时候,还是有点口语化,随意。
先看着。

我尽量先把我想到的问题都写出来,过后再找时间,再修改好。

PS:这些都是一些其实很小巧的边边角角的东西,不是什么大不了的东西。
但是,就我自己个人而言:如果我早点想明白这些,我不会在这上面花那么多时间。
这个折腾的结果是无意义的,但这个过程,我还是觉得有用的。
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

26
 
先开个头吧,下一步讨论的问题:
      封装到哪一步?
      就以上面最后讨论到的 i2c的开始时序为例。

其实我们都知道,最终我们操作i2c的时候,我们只关心通过i2c总线写出什么数据,或者读到什么数据。
至于说它的时序怎么产生,需要什么基本时序搭配(i2c基本时序就五种,开始信号,结束信号,写信号带ACK,写信号不带ACK,读信号)这种底层的东西,我们根本不关心。

所以,你还打算对外暴露 i2c_start() i2c_stop() 这样的函数接口么?

问题2:
我原来是使用的模拟i2c,现在我改用了MCU的硬件i2c.
那么,我原来要配置的是io口,现在我变成了要配置外设i2c的寄存器。

想一想,如果这个时候,你对外的接口不是 i2c_read,或者i2c_write,一般还可能加个 i2c_ioctl之类的。
而是什么i2c_sda_high(),i2c_sck_low();的话
你会不会特别崩溃?

因为,硬件i2c,根本就不用到你直接操作 sda sck啊。

这个时候,移植就出现了接不上趟的接口层面。
你最后不得不干的事情,一定是重新实现一套

i2c_start(),i2c_stop()......

所以,既然如此,你为什么不干脆从一开始就不暴露出 i2c_start() 的实现和接口呢?
只给 i2c_read/write 不是很美好吗?
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

27
 
总结一下:
所谓寄存器映射,底层封装这件事,简单地说就是,BSP一包到底。
而且BSP包里直接堆代码,一步到位。

比如前面说的,不要跟我分什么 GPIO的A B C D了,也不要分什么 PUPD是01,PULL-UP是02之类的;
(当然,在BSP的具体实现里为了好理解和修改,你可以这么写),但是,这些东西绝对不要作为参数放在函数声明里了。

就是说,在BSP里你可以gpio_cfg(1,2,3,4....)但是到了BSP对外提供的接口里,我一个参数都不想要,只想要 led_on()或者 spi_read()

第二个问题:封装到哪一步?
通常而言,原则是:任何外设,都可以看成一个外部存储器,所以你只要封装到可以对它的读写(即所谓读写总线)就可以了。
比如,led可以看成一个 只写位寄存器;
switch是只读位寄存器;
而SPI,串口这种则很明显是一个(或一对)可写可读BUFFER
通常来说,这种层次的封装已经足够底层,不需要再往下了。但往上走则要考虑。例如一个lcd屏,它的接口可能是SPI。
如果我们对外提供了它的SPI读写接口,我们可以直接按照数据手册,对其读写命令,完成显示。但是——对于LCD屏而言,我们真的需要暴露这么细节的接口吗?
其实不需要,第一,对于一块特定的LCD(驱动IC),它的接口、指令集和寄存器是固定的,不管怎么折腾它,你要做的不过是在上面画点画线写字。
所以,如果是我,我连SPI接口都不开放,直接提供画点函数就OK了。

好处是很多的,因为你下次换个屏,说不定已经是8080接口了,你的指令也完全不一样了,你的寄存器更是不一样,但凡你的封装层次出现这三个上面的任何一层,你原来写的任何LCD操作API都将重写和面临作废。

但有的时候,事有不同。
比如说,还是说回LCD的封装,我们知道,其实只要有画点函数,就可以完成任何画线画块写字API。
uCGui就是这么干的。它的底层移植其实相当简单。
就只要在 lcd_dummy或者其他对应驱动ic的 lcd_xxx.c里,改掉画点函数即可。
看起来非常完美,但是面临一个问题:
但我们用uCGui绘制图片的函数的时候,我们发现,它的速度非常慢,我测试过,和自己写的,足足可以相差好几倍。
后来我找到原因所在。
如果我自己画图片,我用的是 先 设定一个填充区域,再依次写入数据。
但uCGui则不是,它是一个点一个点写(中间多了每次要重新定位点坐标),一旦像素点多起来(比如240*240)这个差异就变得很明显。
我们的解决方法是,修改掉uCGui底层的画线函数。
但这也给我们一个启发:
尽管,封装BSP,或者说封装任何东西,我们最希望达成的目标,通常是,接口越少越好,不管是函数还是传递进去的参数。
但是,在具体的条件下,事情却并非如此。
一定要根据实际条件操作。

这个原则是什么呢?
我想,这个原则最简单的表述就是:事情本来是怎样就怎样,事情本来该怎么做就提供什么样的做法。
比如LCD,我本来就可以提供块写功能,为什么就不能开放这个接口呢?
又比如LED,我本来就只是不亮就灭,那你就只提供这个就好了呀,别的都不需要折腾啊。
需要指出的是,这个事情,指的是用户,在这里,用户指的是,接口调用者的角度出发的。
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

28
 
写完这些,不免会突然好奇,为什么那么多人喜欢这么干,包括曾经的自己。
于是我首先找找我印象里,那些也这么干的厂商例程 或者 一些其他库。

比如这个,TI CC2530的 IO口封装,虽然仍然是8051内核,但是它的寄存器也已经比原来的复杂了不少。



  1. //----------------------------------------------------------------------------------
  2. //  Macros for internal use (the macros above need a new round in the preprocessor)
  3. //----------------------------------------------------------------------------------
  4. #define MCU_IO_PERIPHERAL_PREP(port, pin)   st( P##port##SEL |= BM(pin); )

  5. #define MCU_IO_INPUT_PREP(port, pin, func)  st( P##port##SEL &= ~BM(pin); \
  6.                                                 P##port##DIR &= ~BM(pin); \
  7.                                                 switch (func) { \
  8.                                                 case MCU_IO_PULLUP: \
  9.                                                     P##port##INP &= ~BM(pin); \
  10.                                                     P2INP &= ~BM(port + 5); \
  11.                                                     break; \
  12.                                                 case MCU_IO_PULLDOWN: \
  13.                                                     P##port##INP &= ~BM(pin); \
  14.                                                     P2INP |= BM(port + 5); \
  15.                                                     break; \
  16.                                                 default: \
  17.                                                     P##port##INP |= BM(pin); \
  18.                                                     break; } )

  19. #define MCU_IO_OUTPUT_PREP(port, pin, val)  st( P##port##SEL &= ~BM(pin); \
  20.                                                 P##port##_##pin## = val; \
  21.                                                 P##port##DIR |= BM(pin); )

  22. #define MCU_IO_SET_HIGH_PREP(port, pin)     st( P##port##_##pin## = 1; )
  23. #define MCU_IO_SET_LOW_PREP(port, pin)      st( P##port##_##pin## = 0; )

  24. #define MCU_IO_SET_PREP(port, pin, val)     st( P##port##_##pin## = val; )
  25. #define MCU_IO_TGL_PREP(port, pin)          st( P##port##_##pin## ^= 1; )
  26. #define MCU_IO_GET_PREP(port, pin)          (P##port## & BM(pin))

  27. #define MCU_IO_DIR_INPUT_PREP(port, pin)    st( P##port##DIR &= ~BM(pin); )
  28. #define MCU_IO_DIR_OUTPUT_PREP(port, pin)   st( P##port##DIR |= BM(pin); )
复制代码


光这一段,只是IO口的,不知道你是不是已经看到觉得很累了。
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

29
 
这类方式非常常见,包括Arduino,ST库。
尽管各家略有差异,但是本质思想其实并没有区别。

我试图找出为什么大家都喜欢这样做的原因:
1.受底层寄存器本身的结构所启发:大多数都是一个外设,分若干个寄存器,寄存器是按位定义。而我们开发单片机的人,通常会直接面对寄存器(了不起经过ST库这样一层封装),或者像厂商外包程序员,他们本来的职责,就是面对底层做个封装,他们不能也不会面对一个具体的板子或者任务,没有任何具体的封装指向。
2.我们都希望它们可以通用,怎么配置都可以。而且最好是适应不同的系列,甚至不同的厂商MCU。

在这种情况下,早期我们甚至可能会希望实现类似
同样一个 8按键+3数码管的简单外围电路。
我们可以写出一套 HAL封装,使其可以简单地“兼容”从 8051的 P01 P11 P23 到 STM32的 PA3 PB3 PC4......

于是,通常会闹出这样一种所谓的 “抽象底层”:
一起讨论:如何做一个单片机程序通用模版
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

407

帖子

3

TA的资源

纯净的硅(初级)

30
 
辛昕 发表于 2017-11-18 16:00
这类方式非常常见,包括Arduino,ST库。
尽管各家略有差异,但是本质思想其实并没有区别。

我试图找出为 ...

如果真想兼容所有芯片,那每个芯片都有一套自己的定义,就是一个大工程了。到是可以定个框架,以后每个程序都按这个框架写。现在ST、TI都有自己的HAL库了。
 
个人签名我在想
我知道什么
 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

31
 
zmsxhy 发表于 2017-11-18 19:22
如果真想兼容所有芯片,那每个芯片都有一套自己的定义,就是一个大工程了。到是可以定个框架,以后每个程 ...

放弃这种想法。
如果兼容那么痛苦,那为什么不兼容呢?
我忘了是谁说过来着。
我根本不在意通用性,我只想解决复杂度。

我之所以用BSP一包到底,就是因为,我最终彻底放弃了兼容所有芯片这个想法。

甚至同一个芯片,换一个板子,换一套外设,都没必要兼容。
BSP而已,一次写好,才多久。
比起项目的生命周期,微不足道。
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

32
 

MCU底层寄存器映射:结论

本帖最后由 辛昕 于 2017-11-19 20:34 编辑

幸好,有@zmsxhy的回复,我才想起来,我忘了说结论:

不要尝试写一个通用框架,去兼容所有单片机的底层寄存器。
我的意思是,我之前那个帖子 (一起讨论:如何做一个单片机程序通用模版)
从我自己个人的角度,已经被我放弃。

理由如前所述。
那么多的参数,完全无法屏蔽底层细节,而这些东西又基本上一旦确定就不会再变。
根本不值得为之准备好这些 灵活性 和 通用性。

另外,也是以我个人的实践为例子。
这种方式相对太过复杂——相对它带来的好处,完全不值当。

因此,在这个问题上,我可以来个了结了:

1.放弃“通用框架”的想法;
2.别说换一个MCU,即使换一个板子,也不妨重写一套BSP——但通常极其相似(如果是同一个MCU系列的话)。
3.封装要注意隐蔽底层细节,要从调用者的语境确定封装层次。
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

407

帖子

3

TA的资源

纯净的硅(初级)

33
 
对于单片机来说,每个项目都是差不多的,通信、控制、数据采集、显示、储存等等也就这些东西。我想用一个词“格式”来说我的理解。把每个功能都写成固定格式,以后的项目你只需要关心产品功能定义的实现,而不用太在乎板级的东西了。
 
个人签名我在想
我知道什么
 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

34
 
zmsxhy 发表于 2017-11-20 13:30
对于单片机来说,每个项目都是差不多的,通信、控制、数据采集、显示、储存等等也就这些东西。我想用一个词 ...

举个实际点的例子呗~~
其实我对你说的格式的理解就是:
读写总线
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

35
 
对了,关于这个BSP一包到底的问题。
让我想起,此处应该艾特 @sji2000 大锅,当然他可能早就不在意或者解决了。@sjl2001
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

407

帖子

3

TA的资源

纯净的硅(初级)

36
 
或者也可以叫做培养自己的习惯、思维定式之类,我其实只是有个模糊的想法。
比如printf,我们不管是换什么样的芯片,使用哪个UART,都会想把这个方法实现出来。那么其它的功能是不是也可以这样。
或者有没这个必要,看个人喜好了。
 
个人签名我在想
我知道什么
 
 

回复

1908

帖子

7

TA的资源

五彩晶圆(高级)

37
 
辛昕 发表于 2017-11-20 14:13
对了,关于这个BSP一包到底的问题。
让我想起,此处应该艾特 @sji2000 大锅,当然他可能早就不在意或者解 ...

看了你的帖子真的能学到不少东西

可能我现在关注的都是一些教学 科研项目 自己已经很少写具体代码了,很怀念当时自己写代码憋程序的时光。

感谢还记得我
 
个人签名

在技术突破的道路上没有终点

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

38
 
zmsxhy 发表于 2017-11-20 17:30
或者也可以叫做培养自己的习惯、思维定式之类,我其实只是有个模糊的想法。
比如printf,我们不管是换什么 ...

printf 和 uart 是个好例子。
PC上怎么实现,或者说C标准库里的标准流怎么实现,没看过也没研究。

但是,像你说的,不管哪个MCU,哪个串口号,都一样把它实现。
我觉得问题不大。
因为它需要的基础就是 int uart_putc(char c); 和 char uart_getc(void);

那么,我的意思就是。
不管是用 什么MCU,可以是51,也或者是 STM32,MSP430
不管是串口0还是串口1串口2.

既然这个板子已经设计好了,我也就确定了也就知道了。
我就直接把 串口X的寄存器设置,库函数调用什么的,直接实现成

getc putc 里面好了。

我不会再另外在这些函数里加入参数,用来选择串口号或者波特率之类的参数。

如果非要改,我宁可搞一个 iostream_ioctl(int baud,int parity,......) 之类的。
但是,那是对于比较复杂的板子和系统的,一般的情况下,谁会没事改debug口和其它普通通信口的顺序呢?
因此就像我前面说的,既然如此,这个iostream_ioctl()也就基本不会被调用到,也就直接不用留这个借口。

改板子这种事情发生的时候,直接改BSP里的 putc,getc就好了。

也不要为了这个根本不会用到的情形,在 uart_printf之类的函数里留出这个参数。
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复

7815

帖子

56

TA的资源

裸片初长成(中级)

39
 
sjl2001 发表于 2017-11-20 17:42
看了你的帖子真的能学到不少东西

可能我现在关注的都是一些教学 科研项目 自己已经很少写具体代码了, ...

哈哈,说啥捏,也就是换了这份工作后,联系少了,以前还聊的挺多的。
不过两年左右
 
个人签名

强者为尊,弱者,死无葬身之地

 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/8 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表