找回密码
 注册
关于网站域名变更的通知
查看: 384|回复: 1
打印 上一主题 下一主题

高手讲解系列!闲谈嵌入式编程的复杂性

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2019-2-27 09:20 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
高手讲解系列!闲谈嵌入式编程的复杂性4 j$ @$ G3 c# K! w
7 r5 V) W) L' I' l2 F6 d4 S

' b" J8 K& Q2 u* w8 U/ ?能从PC机器编程去看嵌入式问题,那是第一步;学会用嵌入式编程思想,那是第二步;用PC的思想和嵌入式的思想结合在一起,应用于实际的项目,那是第三步。很多朋友都是从PC编程转向嵌入式编程的。在中国,嵌入式编程的朋友很少是正儿八经从计算机专业毕业的,都是从自动控制啊,电子相关的专业毕业的。这些童鞋们,实践经验雄厚,但是理论知识缺乏;计算机专业毕业的童鞋很大一部分去弄网游、网页这些独立于操作系统的更高层的应用了。也不太愿意从事嵌入式行业,毕竟这条路不好走。他们理论知识雄厚,但缺乏电路等相关的知识,在嵌入式里学习需要再学习一些具体的知识,比较难走。0 X$ n( ~+ v* n8 T; T" x
7 W, F3 R8 b/ @4 ~
虽然没有做过产业调查,但从我所见和所招聘人员,从事嵌入式行业的工程师,要么缺乏理论知识,要么缺乏实践经验。很少两者兼备的。究其原因,还是中国的大学教育的问题。这里不探讨这个问题,避免口水战。我想列出我实践中的几个例子。引起大家在嵌入式中做项目时对一些问题的关注。
; w' Q4 w( H' C4 H
; s; H( W* a9 [$ o0 L0 b$ U第一个问题:
- ~* O; @+ u/ {. \# Q' M: M, r! p3 j  N# a( G3 K
同事在uC/OS-II下开发一个串口的驱动程序,驱动和接口在测试中均为发现问题。应用中开发了个通讯程序,串口驱动提供了一个查询驱动缓冲区字符的函数:GetRxBuffCharNum()。 高层需要接受一定数量的字符以后才能对包做解析。一个同事撰写的代码,用伪代码表示如下:
" z3 S  Z5 L- d/ }3 `- m5 X) b3 W2 ^4 E' _" M
bExit = FALSE;, G% P% T6 Z3 }7 E' w, Y9 u
  [5 r" I8 P$ s  E$ E" e5 a8 b0 ^
do {
- \6 ~; z. W( A8 y+ m) [7 E& Y6 z! e4 R) T1 B& m0 K2 M5 {
if (GetRxBuffCharNum() >= 30)
- z' a3 W* b& ?
0 g- _$ \; L# {1 V! fbExit = ReadRxBuff(buff, GetRxBuffCharNum());
5 s# v  g2 t% f% T! N- m. D- u7 y
} while (!bExit);3 s/ w9 t, W: Y) E9 X: k) r& s
& q/ P2 f: M7 h; q4 E
这段代码判断当前缓冲区中超过30个字符,就将缓冲区中全部字符读到缓冲区中,直到读取成功为止。逻辑清楚,思路也清楚。但这段代码是不能正常工作。如果是在PC机上,定然是没有任何问题,工作的异常正常。但在嵌入式里真的是不得而知了。同事很郁闷,不知道为什么。来请我解决问题,当时我看到代码,就问了他,GetRxBuffCharNum()是怎么实现的?打开一看:
+ e! F( T5 E+ s( L& ~. q9 X/ L
unsigned GetRxBuffCharNum(void)
5 L( H# i6 G4 @. c" r8 q9 h6 q% s- T: C( T' |! J8 `; s; |& c
{
/ H* H8 `) Q7 T/ l: r  w9 R6 a5 p
6 j+ a  G& l- U0 s6 {+ u5 D2 Lcpu_register reg;
1 f, c1 _  N( x, d7 g4 H) y, p" C: K5 G: L" W9 Q
unsigned num;4 `- }' w. S6 v5 f
# f0 ?4 Q5 ?: ^) A# R$ w
reg = interrupt_disable();
! O* y( [  S" b% A+ G9 K9 I( K$ _& L! y% n9 a% ^5 Z
num = gRxBuffCharNum;
1 ^) Z: @' d$ F/ i6 ?: X9 y! S' b' ?; L/ W# w! e7 {
interrupt_enable(reg);
5 k0 j* a' a5 E' a; S1 \" P. U. a: N1 g
return (num);: X8 {5 v: w7 p: E2 l
" H$ r8 r& O* V& A- B$ ~: l: s
}
$ u5 Y0 A4 V9 H: j- J6 ]/ ^+ G) Z6 d7 M  u5 }! L
很明显,由于在循环中,interruput_disable()和interrupt_enable()之间是个全局临界区域,保证gRxBufCharNum的完整性。但是,由于在外层的do { } while() 循环中,CPU频繁的关闭中断,打开中断,这个时间非常的短。实际上CPU可能不能正常的响应UART的中断。当然这和uart的波特率、硬件缓冲区的大小还有CPU的速度都有关系。我们使用的波特率非常高,大约有3Mbps。uart起始信号和停止信号占一个比特位。一个字节需要消耗10个周期。3Mbps的波特率大约需要3.3us传输一个字节。3.3us能执行多少个CPU指令呢?100MHz的ARM,大约能执行150条指令左右。结果关闭中断的时间是多长呢?一般ARM关闭中断都需要4条以上的指令,打开又有4条以上的指令。接收uart中断的代码实际上是不止20条指令的。所以,这样下来,就有可能出现丢失通信数据的Bug,体现在系统层面上,就是通信不稳定。& d7 F5 f5 u2 k; B% d' i

# b3 I5 B# Z4 L6 w. i# [修改这段代码其实很简单,最简单的办法是从高层修改。即:; p! u& P3 ], W& P" A+ a2 E
  Y; x& o5 f7 F5 |6 [& I
bExit = FALSE;
) N# u5 x: g* H( e  L3 l9 T9 y5 i; x) J8 A
do {9 R) }( h6 D+ M( X% n8 K0 _# e2 r

0 }" |$ g- W" lDelayUs(20); //延时 20us,一般采用空循环指令实现
) [, V( ?5 @3 h" u0 [$ c+ V8 }4 f
% ^! U  I# S3 q, c5 tnum = GetRxBuffCharNum();
  f% q% Y+ u, `# f; ?. o- q, {8 ?9 h. i* J; Y
if (num >= 30)" L) F& L# T# b4 q' h1 n( C

- W- Y: v) |; G8 c: e* F: C' YbExit = ReadRxBuff(buff, num);
3 P& G7 |; m" p- i" U; R# k6 f0 y8 G$ y/ Z7 n$ h
} while (!bExit);
! H0 j( O3 P( L, h2 z+ o; c0 @
% q4 I8 a3 S7 X! r1 H这样,让CPU有时间去执行中断的代码,从而避免了频繁关闭中断造成的中断代码执行不及时,产生的信息丢失。在嵌入式系统里,大部分的RTOS应用都是不带串口驱动。自己设计代码时,没有充分考虑代码与内核的结合。造成代码深层次的问题。RTOS之所以称为RTOS,就是因为对事件的快速响应;事件快速的响应依赖于CPU对中断的响应速度。驱动在Linux这种系统中都是与内核高度整合,一起运行在内核态。RTOS虽然不能抄袭linux这种结构,但有一定的借鉴意义。7 I& p# E: Q- u$ [, \8 k

( a/ o$ |4 D1 B$ O" K# c6 s从上面的例子可以看清楚,嵌入式需要开发人员对代码的各个环节需要了解清楚。
4 Q  ~4 q" O- b' W+ |
& G* H2 h& ^6 j/ a% p1 \& E9 G第二个例子:
$ }' t% X* S. P. k) H
4 i5 m; z! G, o/ p) \( I同事驱动一个14094串转并的芯片。串行信号是采用IO模拟的,因为没有专用的硬件。同事就随手写了个驱动,结果调试了3、4天,仍旧是有问题。我实在看不下去了,就去看了看,控制的并行信号有时候正常有时候不正常。我看了看代码,用伪代码大概是:
8 D6 H( l# ]/ M$ e( v+ y& ^& t# ~, p8 r; b5 I: V9 D: ~
for (i = 0; i < 8; i++)1 j% j+ j; Q7 u+ Y

6 J1 l7 F- q6 G0 l{1 `4 f2 F- k+ f1 v7 c5 S( u

" S2 d/ R" V2 e) oSetData((data >> i) & 0x1);
, E) c/ @% L0 H) e( t4 @* Q
+ e2 z9 p+ J! o# o7 Z$ ~- wSetClockHigh();$ K1 ?7 l. b$ h' \
  W( Z9 k+ D( c7 t3 a
for (j = 0; j < 5; j++);
1 p8 D' Q7 @. m" \8 g1 W+ \" P* f  M3 o7 l: A' E
SetClockLow();( Z* `1 v# ^2 Z4 U/ W0 }5 H

  t5 d8 t0 m: R4 F, ^}5 |, a6 J. l" G$ M- G/ [
5 M5 @" E/ |8 D4 x1 N
将数据的8个bit在每个高电平从bit0到bit7依次发送出去。应该是正常的啊。看不出问题在哪啊?我仔细想了想,有看了14094的datasheet,明白了。原来,14094要求clock的高电平持续10个ns,低电平也要持续10个ns。这段代码之做了高电平时间的延时,没有做低电平的延时。如果中断插在低电平之间工作,那么这段代码是可以的。但是如果CPU没有中断插在低电平时执行,则是不能正常工作的。所以就时好时坏。
8 u; c. a% @1 @- v* q# V+ F* [- t! |: w1 J% {) X
修改也比较简单:
0 k) B$ b; A9 k
$ U8 f) }! c- w6 y' e* r" kfor (i = 0; i < 8; i++), F8 \+ B9 i2 ^$ z  U

; G! |, e! H0 c{
: H; W4 [$ X8 j' E* W! z0 M
$ Z8 [+ d8 V% {3 f" kSetData((data >> i) & 0x1);3 P: J3 L. N" W: p+ q

% B0 O6 d/ }# SSetClockHigh();# l! a4 P& M4 H$ ^5 a! Q
) z/ L. f! ]7 b7 x
for (j = 0; j < 5; j++);: b3 _/ [# j% h4 V$ x  W
4 e1 q& r8 ?' q0 O1 @/ X
SetClockLow();
: i8 z' D; P; Y3 P8 F, B1 k& F! j; e. ]1 P$ ~
for (j = 0; j < 5; j++);
9 ~. w* `8 ?% w7 l3 N: j3 ]# J: b; Y, v  r: j( U
}" ]/ C2 x+ }8 F; k0 d8 W
5 t& b. Z9 m5 v1 k$ U* r
这样就完全正常了。但是这个还是不能很好移植的一个代码,因为编译器一优化,就有可能造成这两个延时循环的丢失。丢失了,就不能保证高电平低电平持续10ns的要求,也就不能正常工作了。所以,真正的可以移植的代码,应该把这个循环做成一个纳秒级的DelayNs(10);
) H5 y) g( z- F6 T( Y7 r* j  n) j$ v1 Q7 o; t, ]# ~. q( a% D7 }
像Linux一样,上电时,先测量一下,nop指令执行需要多长时间执行,多少个nop指令执行10ns。执行一定的nop指令就可以了。利用编译器防止优化的编译指令或者特殊的关键字,防止延时循环被编译器优化掉。如GCC中的
5 K& s2 E+ A& h' {7 Y7 [
9 x. Z+ b1 f; P) Q1 G1 Z$ O# r7 S__volatile__ __asm__("nop;\n");
  a2 n0 `* S0 n5 I6 a) F  l- f# J& N' U. k; q9 Y
从这个例子中可以清楚的看到,写好一段好代码,是需要很多知识支撑的。

0 C2 _% f6 g9 s1 p
5 {) p- X  L# w9 w  i3 V4 A! M

该用户从未签到

2#
发表于 2019-2-27 10:31 | 只看该作者
说的很不错,谢谢分享
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-7-21 01:05 , Processed in 0.109375 second(s), 23 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表