|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
高手讲解系列!闲谈嵌入式编程的复杂性
) @+ I L' K8 L0 G+ N( ~, E$ u7 O0 P. }- f
; K" }/ p4 f$ m( N% h0 L1 V
能从PC机器编程去看嵌入式问题,那是第一步;学会用嵌入式编程思想,那是第二步;用PC的思想和嵌入式的思想结合在一起,应用于实际的项目,那是第三步。很多朋友都是从PC编程转向嵌入式编程的。在中国,嵌入式编程的朋友很少是正儿八经从计算机专业毕业的,都是从自动控制啊,电子相关的专业毕业的。这些童鞋们,实践经验雄厚,但是理论知识缺乏;计算机专业毕业的童鞋很大一部分去弄网游、网页这些独立于操作系统的更高层的应用了。也不太愿意从事嵌入式行业,毕竟这条路不好走。他们理论知识雄厚,但缺乏电路等相关的知识,在嵌入式里学习需要再学习一些具体的知识,比较难走。
5 r7 |8 r+ P5 ?0 \
; J6 w. _* Q' P1 F6 Z7 s) ]3 \虽然没有做过产业调查,但从我所见和所招聘人员,从事嵌入式行业的工程师,要么缺乏理论知识,要么缺乏实践经验。很少两者兼备的。究其原因,还是中国的大学教育的问题。这里不探讨这个问题,避免口水战。我想列出我实践中的几个例子。引起大家在嵌入式中做项目时对一些问题的关注。$ I9 y: y- l! ~7 l" N7 W4 I( P
: A$ J4 O8 P0 S ^$ U
第一个问题:: a# V: ] J5 u( W" f; ^: F
7 n6 l+ n/ k% ] d同事在uC/OS-II下开发一个串口的驱动程序,驱动和接口在测试中均为发现问题。应用中开发了个通讯程序,串口驱动提供了一个查询驱动缓冲区字符的函数:GetRxBuffCharNum()。 高层需要接受一定数量的字符以后才能对包做解析。一个同事撰写的代码,用伪代码表示如下:5 s% P9 k- ^* \
8 W3 m& a1 E* abExit = FALSE;. J. r- j L2 W5 u7 k2 c7 f
: S% k# t2 b3 a! c+ N& {do {0 ^7 W! u+ | f$ G% v
+ [# M7 V, s. ~4 @3 H% eif (GetRxBuffCharNum() >= 30)) d) S. I5 R$ v* V; x/ H) U
A$ h/ N6 o. w [9 W l
bExit = ReadRxBuff(buff, GetRxBuffCharNum());
- h# x' N, S6 Y4 a+ s
- |( l$ m, x5 d( e9 W} while (!bExit);
' Q- g) [, H: |1 t" T" P$ y
O! \- K H3 I+ p6 `+ }这段代码判断当前缓冲区中超过30个字符,就将缓冲区中全部字符读到缓冲区中,直到读取成功为止。逻辑清楚,思路也清楚。但这段代码是不能正常工作。如果是在PC机上,定然是没有任何问题,工作的异常正常。但在嵌入式里真的是不得而知了。同事很郁闷,不知道为什么。来请我解决问题,当时我看到代码,就问了他,GetRxBuffCharNum()是怎么实现的?打开一看:
9 J2 D9 s9 p. ~7 G* c0 R
' v: j V/ f/ G {* D- s zunsigned GetRxBuffCharNum(void)7 X4 ]8 p) n/ g* T5 p% v7 J
' N. A I+ O! V6 L- g{/ j! X# q( f5 ]6 L5 W: \3 p3 {8 o
# f- R7 R& _+ w7 w9 f+ Pcpu_register reg;
0 r2 G% i9 J! ] I" v" s
5 D j, ^4 M5 y0 P2 f2 M9 munsigned num;
9 W3 I8 Y/ E5 }2 s' n! R
4 Q% N5 T* {2 k2 V: x, x. rreg = interrupt_disable();
) M$ z; q# }7 E( `( l
5 s/ |4 V. V3 P' [/ xnum = gRxBuffCharNum;
. `6 x' {+ V3 H: q0 Y' |1 T
Y. O# ]; Q) b$ {- Z. `" x6 ]interrupt_enable(reg);
- T$ C; S; S2 t* U X
! O5 ~9 T m" j! F5 z& w y: @8 }return (num);
6 S `! Z3 E V
; t' Z& U; ~9 r6 b' C}0 ~' k/ W( e+ v. G! O+ Z
$ Y5 U" y4 ^5 S3 b
很明显,由于在循环中,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,体现在系统层面上,就是通信不稳定。) z9 W) A, C M$ ~, Q
! \2 Z& N! `0 k
修改这段代码其实很简单,最简单的办法是从高层修改。即:
e( M6 u# K: f1 `- g7 M* L/ ^5 I3 U- S$ C( n$ g
bExit = FALSE;
g5 X! W4 ~% v' m) [
# |4 T( |2 h7 m/ U( S: X' xdo {
4 Z; }8 Z( o; R0 z7 p0 c8 L4 q! m/ E0 P* y
DelayUs(20); //延时 20us,一般采用空循环指令实现, D7 @7 D* {8 w1 c
w' @. h; \$ m3 _num = GetRxBuffCharNum();- ~+ e$ c& k$ T; c
4 i) p4 d, i* r, Z5 K
if (num >= 30)
. L# U G: L8 R+ j" i5 z6 S
% v! d: p; F. L; X: TbExit = ReadRxBuff(buff, num);0 [& q% T9 F6 v i# t0 F
: v2 {% s$ ?7 @: Q7 ^1 W
} while (!bExit);4 R, f- i$ C. a( l
7 t/ K0 I: v* d# u) i$ m: K
这样,让CPU有时间去执行中断的代码,从而避免了频繁关闭中断造成的中断代码执行不及时,产生的信息丢失。在嵌入式系统里,大部分的RTOS应用都是不带串口驱动。自己设计代码时,没有充分考虑代码与内核的结合。造成代码深层次的问题。RTOS之所以称为RTOS,就是因为对事件的快速响应;事件快速的响应依赖于CPU对中断的响应速度。驱动在Linux这种系统中都是与内核高度整合,一起运行在内核态。RTOS虽然不能抄袭linux这种结构,但有一定的借鉴意义。8 }$ T Y% C8 O
# r1 ^9 J& i- ~1 P" X8 [/ c! ~& C
从上面的例子可以看清楚,嵌入式需要开发人员对代码的各个环节需要了解清楚。
; U6 p8 Z2 T5 n8 i% D" ?8 I- T* @. W; e$ ]
" q2 F* C" f' Y: c第二个例子:
5 q* ]; w! u* g+ R" E6 G& E( q2 M# Q
同事驱动一个14094串转并的芯片。串行信号是采用IO模拟的,因为没有专用的硬件。同事就随手写了个驱动,结果调试了3、4天,仍旧是有问题。我实在看不下去了,就去看了看,控制的并行信号有时候正常有时候不正常。我看了看代码,用伪代码大概是:( d; Y, L" I2 j) D- n
! q- H: V- W( g/ b* u' v
for (i = 0; i < 8; i++)
9 j! c/ u S6 X4 X- V2 Z+ ?
# H! w* C x3 E) L+ x1 R{
( k. @! R) Z9 A, w9 T6 G E4 d$ @) T& ~' G* E, I" s1 d
SetData((data >> i) & 0x1);
3 x" I# ]3 C8 X6 ~1 F- ]7 ]8 L1 b" N+ n/ b
SetClockHigh();/ L5 T# }7 f. V4 Z+ A
+ O7 J( G9 z8 q dfor (j = 0; j < 5; j++);5 G3 k+ {$ }& x: n5 ~8 C
" S8 _" w4 _& @
SetClockLow();* }$ ^, t, q0 A$ Q' i
8 X8 \; [$ A, |}
! M0 R3 L$ ^2 H! B" z: _( n1 x; u' C6 `1 H: a+ u
将数据的8个bit在每个高电平从bit0到bit7依次发送出去。应该是正常的啊。看不出问题在哪啊?我仔细想了想,有看了14094的datasheet,明白了。原来,14094要求clock的高电平持续10个ns,低电平也要持续10个ns。这段代码之做了高电平时间的延时,没有做低电平的延时。如果中断插在低电平之间工作,那么这段代码是可以的。但是如果CPU没有中断插在低电平时执行,则是不能正常工作的。所以就时好时坏。' O. Q7 Q6 @: e
9 ?+ t9 F3 h7 d修改也比较简单:
- `, I7 B* X: x1 D# [5 ]0 Z# i) _6 `9 m1 ]( e# s6 c' ^8 s3 r$ a
for (i = 0; i < 8; i++)
& z9 S- H' ^/ I" [
: ?% G" C3 B! H) @, e6 I{
+ I( O* @3 w/ w# h7 y% J" q/ f# w! P" ~( V, V6 t$ ~
SetData((data >> i) & 0x1);" r: h7 o1 v) x) ^) h/ i6 W t+ o6 X
W8 b( |; [! F/ P& iSetClockHigh();
7 }7 a. z& p" b8 D2 X7 P
" {9 G/ E( d& x+ O& Ufor (j = 0; j < 5; j++);
8 W4 K2 v& G: c, `+ X) f! v' _5 Z _1 r3 _0 Y. `+ }
SetClockLow();! k6 c2 {# a# b6 U3 q( [: ^1 r
: j" H2 j e- {& V4 I% _* _$ ~for (j = 0; j < 5; j++);6 f6 g8 R4 h5 m/ R) ?( k
7 {- j, U" R% M- j}
5 F2 s6 ?6 X7 V) m" [
3 U* @* u: Z' {* \+ `这样就完全正常了。但是这个还是不能很好移植的一个代码,因为编译器一优化,就有可能造成这两个延时循环的丢失。丢失了,就不能保证高电平低电平持续10ns的要求,也就不能正常工作了。所以,真正的可以移植的代码,应该把这个循环做成一个纳秒级的DelayNs(10);
; ?) J% K* l6 L
* v( y1 }2 ^) |8 Q像Linux一样,上电时,先测量一下,nop指令执行需要多长时间执行,多少个nop指令执行10ns。执行一定的nop指令就可以了。利用编译器防止优化的编译指令或者特殊的关键字,防止延时循环被编译器优化掉。如GCC中的
% n2 s) }, H0 C, Z- Y; i8 s6 U. j2 G. e1 F" Y1 M5 t% R
__volatile__ __asm__("nop;\n");; i$ o; \ g! K( e" Q4 P
2 f7 C4 y8 X! d
从这个例子中可以清楚的看到,写好一段好代码,是需要很多知识支撑的。
) \ r9 j( p/ d3 e
5 d7 Q; P Q) V6 v( g |
|