你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

Ubuntu下开发STM32---5.使用串口Part1 精华

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑 9 E2 u  J8 N7 w" s% C+ i4 y
2 E9 c$ i0 ~! S* d. z6 z
  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。7 a5 R! Y* k* w9 h# o  E& ]

7 _0 C5 g( B, F3 V4 ?0 s使用ringbuffer
) H/ L* ^. Q* ~' \' _& U这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。. I- y% j0 d" t  K+ L) U; X2 u$ d
struct ringbuffer( t, W9 N% n9 Q1 @2 F8 P& g( P5 s
{" |3 g0 j& V* l& x+ g0 x, ~7 v
    uint8_t *bf;
& f4 P9 N& N6 K    int len;4 _8 g2 u% B- l0 |4 q' O! i
    : o4 M) d" b  J0 p
    int count;* a' G, i4 V  w; {! r) r
    int putidx; /* read index and put index */
$ e1 g" u7 \0 [$ W    int getidx;
& l, @* U% W& I& ~: g};

# q' L! J! n6 u5 b; \$ x- sstruct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。
  G  v; W# U0 X# ?4 g1 J#define DECLARE_RB(name,len) \
& [" n7 t4 [  V" w" `% K    uint8_t name##_buff[len]; \
, Z! v$ T3 v# Q; B    struct ringbuffer name = { \( L! y8 M' H1 D% w, V3 {
        name##_buff,len,0,0,0 }

4 k! c: t, n) _! z: G提供的接口函数:9 W- Q. o5 h0 C( Y: @
#define rbcount(prb) ((prb)->count)
: f8 J  j* x7 v4 ^#define rbfull(prb) ( (prb)->count==(rb)->len )
- L; I; E& j4 D# B' Y' E$ Z" K#define rbempty(prb) ( (prb)->count==0 )
3 i" [& k* w! B6 h! E: q! T& ]# @7 A5 L5 o4 v7 z
/* if ringbuffer is full,return 0.else return 1 */9 I7 O9 F' N! d- |8 s4 }- f
int rbput(struct ringbuffer *prb,uint8_t add);6 d$ p1 W2 ^9 f& v4 O
/* if ringbuffer is empty,return -1.else return bf[tail] */
. A4 {! m3 D. C$ Eint rbget(struct ringbuffer *prb);
" `/ I! d* G6 `, F" h  c8 C

7 }) f7 i8 P) A0 F( X7 b$ @rbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
  F, J" m  w# b! e3 [#include "ringbuffer.h". H! g% H  N! g/ M& ~; J

& X) c, x  M3 A: N. i( Z/ _int rbput(struct ringbuffer *rb,uint8_t add)
" j  Y0 S* y: Q% i& G4 G{
8 b, T0 q5 C1 o+ [. S, E+ z8 w    int curidx=rb->putidx;
! J% A7 |0 @+ k& }7 v4 @6 W    if(rbfull(rb)) return 0;7 e0 l% g% a7 n9 O! D! ]
    rb->bf[curidx]=add;* q: A; f! z' J2 a
    rb->putidx=(curidx+1)%rb->len;2 h# G% |$ ^, `; P$ ~3 w
    rb->count++;/ X7 {. Q' I4 `/ a" W, x% ?. k
    return 1;; a* u" s) i, A$ K/ p0 m
}
0 t9 S) [& [' m+ x1 U+ s& w+ Z8 F+ Q, q5 @+ Y) ^4 D. n
int rbget(struct ringbuffer *rb)' Q& a& I" P2 U- e
{
  i, l( B9 Y4 J# P7 d3 n' d: @5 d    int result;
) X' F& O2 Y+ ~: X0 `4 ]    int curidx=rb->getidx;7 w/ R: g) o) b0 o( o4 R# J
    if(rbempty(rb)) return -1;
( X. R9 Q9 n" @# J    result=rb->bf[curidx];
/ ]6 M/ b# a. B7 F6 p    rb->getidx=(curidx+1)%rb->len;
4 k; I7 a  a2 ], U! S    rb->count--;
! C4 Y3 R+ G2 P3 g2 _% I    return result;
: D) H  M0 i# S5 X. s$ x}: u1 `( h* E' {& Q
+ T% _( O6 q& W: o$ E/ D
串口中断+ringbuffer' j0 C+ d- n& e0 d
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。
3 |2 |" O/ ?. c/ e- _4 d/* USART interrupt handler */! M/ O$ I; Q" R& B2 d- h9 [
USART_Handler()* W2 g* q' j, ^; D
{
! c: o% D+ u" W& k- R9 N    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){
/ V: K6 K- c1 ?2 Z# P9 ^6 {5 X        rbput(&rb_usart,USART_ReceiveData(USART));
% A8 B4 w( n3 _& n: D, C    }0 K9 j& b" Y/ t) S1 t3 @  s
}

; T/ Z0 A; r, Z1 W编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。
9 s) g' B6 U0 ]; u6 I" x4 Y7 s  bDECLARE_RB(rb_usart,USART_RD_BUF_LEN);& z8 |5 I' G" N
+ u0 P/ B- H" D# I* Q6 V+ A/ F
int __io_putchar(int ch)
/ m0 T. Y9 d1 k/ W1 Z{, z0 x7 {% G! e! p3 M7 ~5 E$ F# O
    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
  P% m1 s; y: y) p$ v* v3 `    USART_SendData(USART,ch);& B0 O. Z" Q4 b( l0 ]2 i1 ]" n
    return ch;1 K1 \7 C* O6 E% w3 e
}
9 S0 t/ F: w( ^$ `6 q1 U
$ D* O5 `. z, s, _3 }! eint __io_getchar(void)& @( H/ i. o( B3 z/ h
{
3 y: X  f, z1 s/ m: X) F, Z' z    signed char ch;6 J7 r8 n& u1 T7 i7 p
    while(rb_usart.count==0) continue;
4 g$ |! R: Q, B& t" F5 m4 [1 L/ l    ch=rbget(&rb_usart);
; K* i  g2 R& z) X6 D; Y& M    switch(ch)% K& f0 \5 d: A) i& K- o+ H
    {
4 W7 B( Y6 ^- F; S* a- c5 [        case '\r':ch='\n';break;
- E1 Q% d, W* O( k; C# c        case 0x1b:ch=' ';break;( N$ z. k4 h+ g9 A& [% P$ a: g! m
        default: break;
9 |. Q% I3 \& ]1 o1 a, q: F    }
; _, r/ R1 t4 h! E  S2 K   
: Y/ ^. `! J6 }. t    /* echo it */% v9 e; ^# @2 o
    return __io_putchar(ch);) O7 P" _- ~% x
}
4 }8 \8 d+ D2 j; ^7 Q5 Y
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。' {+ M2 a+ l0 f" P4 q
由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。
1 u# L/ j0 c" O2 H: I
! U" g; P/ b# `! k1 ]4 S在主函数中一直读取字符: d1 Q: N" M+ |8 D3 Z: [- k; S) b
我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。1 z- h4 M$ W7 a/ i) Z
int main(void)
4 l! n9 ~. G- E$ A{; ]6 m0 T5 f5 L  I# ]8 E+ Q% ~
    const char *str="Try to enter something...\n";* N- V, C: `5 t5 [
   
5 E7 K7 @# A; G+ a7 R    const char *tmp=str;2 v: m6 H7 _$ O- ^  e- m% f$ J
    for(;*tmp!='\0';tmp++)
7 z5 O4 b# ^; j: u, A0 q; X6 m; M        __io_putchar(*tmp);( X' @5 o9 `* R0 I
        8 |* t3 u( P* X2 ]! U8 C
    while(1) __io_getchar();
% U4 [" T" Z$ P# v% p( h}
5 o6 P8 Z  g( U1 z8 o  c5 M. p4 v( Q: H! k
void _exit(int status)
$ {5 c9 m4 j6 s5 h{
/ U& h) G/ y" \& I: N: f    const char *exit_str="GoodBye!\n";
' L$ T3 K  y( }6 L, t# y" H    const char *tmp=exit_str;* l3 n# l5 F' }& @" A# W
    for(;*tmp!='\0';tmp++), n3 G0 P% D" F4 m
        __io_putchar(*tmp);
$ x$ N$ u; F7 `5 |    - T: F8 Y9 N) D
    for(;;) continue;: N) F. u& X; x$ m
}
' T  r& n" M* ~5 L  x
# d2 }( W" k4 X* r7 Q
下载到FLASH中运行
9 J# u+ F$ g, k. b/ w目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。
( D' {7 S, N+ c) g9 ^5 A' Q8 E: D试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .
. V3 d5 P2 k1 N5 c% w1 |不管什么原因,至少能够显示也是好的。+ E1 e  f' g- M  N  }

* h+ ~6 L0 y9 S2 W3 J  _: i使用gdb调试代码8 _) R0 h; C$ H
新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。
8 ]+ F2 ^4 f! |& F3 l% v% ? 2015-10-28 21:47:50屏幕截图.png
2 _  n1 S; p$ {在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
0 o9 \8 m  v# @: A4 p在gdb串口使用tar连接远程终端。如下图所示:
3 A8 E, M- v% \9 N 2015-10-28 21:03:36屏幕截图.png # Q" t2 G- C6 s/ \
tar连接完毕之后,使用load命令将代码下载至单片机内。, \# z& R% I/ u1 _3 K5 m4 m2 D
8 @. z+ [8 A# Y% u4 P- m/ X
至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。
( @7 b$ S- ?4 g7 Q: L+ C  x
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。( D; T. W$ o4 ~$ h; ~2 X; w
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。
( O, c/ O5 e  _. o5 m1 ^0 { 2015-10-28 21:11:53屏幕截图.png " U0 l' ?$ j- y! r6 w9 w
在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。& ~. s! l* a: N3 {# U3 N

; T2 O3 G- |2 G+ Y 2015-10-28 21:15:25屏幕截图.png 5 @2 ^7 ]! X, D
在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。. a0 |9 u& l: p$ b% q% \5 y; H
2015-10-28 21:16:33屏幕截图.png
* h8 K1 q! a$ O2 U" f2 ? 2015-10-28 21:17:02屏幕截图.png 8 c  X. Y$ g7 P' A1 L! _
在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。
# t/ e8 s7 o* G 2015-10-28 21:18:21屏幕截图.png
7 \6 |+ B. w) o5 ?; t在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)- X3 e) d0 n- K, z1 u; o
当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)
0 g: u  {: {% S" x& s. T: L$ `) t' f4 R/ p$ ]+ r9 R
2015-10-28 21:22:46屏幕截图.png
: R  z: \* A9 i9 q) n我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。
4 S0 a5 M- l  z; J1 ^( R 2015-10-28 21:26:09屏幕截图.png
' n( @2 g+ U  ]: c% H- I; H4 y使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。1 r0 x$ P7 x' d2 j
  }8 @; u: ?, `4 ]+ j* x4 L* r
如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。
0 N3 e( |0 q" _( }6 d2 [* Y* N: }  e: E+ L6 A/ p

* J7 a0 @1 a) O  W& A
; I5 t: {: W. `: H  Z2 L/ k
bug12 Y# ?) B, t( N  r' S+ g
如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。6 c- i  B. s0 n, f* {9 E0 B5 m7 {
重新make all,make burn测试。
- j. t* b9 m  ?: R6 M 2015-10-28 21:32:23屏幕截图.png * g' m' `7 P- D  U' A* e
问题已经成功解决。更正之后的代码在serial_v2.zip
4 M5 ~9 `% k$ e' a7 z: n! E5 d
bug2
4 j- {& N6 ]# o4 L
bug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。
4 O$ O) }3 K0 r& A: k7 j1 [一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。
7 i6 w) g, f3 C( ~7 Q* r在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:
3 B* w. n5 k' g#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )
% X; K- d5 \. q3 Y0 d#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )
$ X" O+ \3 i& y1 O( F
我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。4 k) L. J2 r- L% j
更新之后的两个实现代码:! {1 O9 a9 o* n
int rbput(struct ringbuffer *rb,uint8_t add)
9 ^7 ~/ M0 l* }# D) P% [{' }) v( j% B1 h
    if(rbfull(rb)) return 0;; E6 ~8 _! j( f9 J) Q' K
    rb->bf[rb->putidx]=add;0 Q1 ~! k7 B1 z7 X) s$ n) h& K
    rb->putidx=(rb->putidx+1)%rb->len;
, e$ S; `! V2 P9 i" `    return 1;
0 t9 a: k' W' v) _- g}5 S0 D/ X% H( Y' |8 y) y2 N
) ]0 Q9 e8 m) V7 k  d5 A
int rbget(struct ringbuffer *rb), K5 S3 W3 x9 `$ n) U, }  \
{
+ u  Z- g! n* ?' u    int result;
. O3 r4 e7 K+ f7 r    if(rbempty(rb)) return -1;
2 ^) s( n' f! A& v/ d- o0 R    result=rb->bf[rb->getidx];
- d* B/ @( @- i( r    rb->getidx=(rb->getidx+1)%rb->len;2 s" Z5 o% a8 q! K& E7 C
    return result;. z5 z! o3 V, P, V9 w% y7 s
}3 Z) E5 w2 P5 }% D: Q2 H' z
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断。- y! }# ?: n1 Q8 h6 k5 F
int __io_getchar(void)0 y. x4 W6 ~$ f8 l7 `+ h  f- |# ]
{# x1 T. B$ `/ c2 u7 m
    signed char ch;
6 w/ |2 s2 C6 v& Q    while(rbempty(&rb_usart)) continue;
2 f% b; ]! h3 }    ch=rbget(&rb_usart);
6 j7 X/ M" w8 C" U. _( _. r9 q    switch(ch)1 S1 u- h( B8 [9 d% Q* f9 X
    {; N' d, f% X( U0 E4 ?
        case '\r':ch='\n';break;8 J; w1 \; I8 F7 h2 d5 Q
        case 0x1b:ch=' ';break;- t0 k$ S6 l6 X* t0 A
        default: break;8 S8 E1 H3 p- @' ~
    }- ]. P7 V: g" V
    7 r- Y/ R$ N! R2 \. H! {
    /* echo it */6 k" L8 a: t0 ?5 B7 W7 d5 a( e
    return __io_putchar(ch);
) |' J! [* g' A. ~) q4 ?. ]}* N1 ^( j9 C2 `
更新之后的代码在附件serial_v3.zip中。
8 i0 X" d! t" E" O4 F
( Y! P0 R: {5 _  `
- \9 x5 _; h$ R* ~- d更多
1 H" _0 J- B% b0 F在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题
。5 F* Y; e  P$ R* X( v/ C

0 {- U! y5 w$ h1 X6 F1 d9 F" e3 H7 Q% e$ |
5 }+ o& E/ t) ^5 O

serial _v1.zip

下载

395.61 KB, 下载次数: 55

serial _v2.zip

下载

395.62 KB, 下载次数: 50

serial _v3.zip

下载

395.61 KB, 下载次数: 75

评分

参与人数 1 ST金币 +30 收起 理由
zero99 + 30

查看全部评分

赞 收藏 3 评论20 发布时间:2015-10-28 21:36

举报

20个回答
Mandelbrot_Set 最优答案 回答时间:2015-10-29 09:08:51
感觉楼主的 rb->count 可能不是安全的.
2 c7 C& x, i8 D- h6 k3 q考虑:
" R# ^6 e3 {; m; s  R+ Erb->count--执行到一半,
: Q3 o$ L% K6 R& i/ z, I0 k5 y此时进入中断rbput里rb->count++
" Z* g, L; k) ^* b可能会导致rb->count计算不对/ J9 u2 W! G. j  {  P) W1 o  ~
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:087 T& {  u4 e, e* t
感觉楼主的 rb->count 可能不是安全的.- U0 ]( E: l; h
考虑:
# G8 U  y+ L$ @+ t+ wrb->count--执行到一半,

# p  w7 M9 f. }0 m确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。
3 w* B# e4 X5 ~+ E+ |% h还需要改改。
党国特派员 回答时间:2015-10-29 09:12:20
学习了。。。 blank.png blank1.png blank2.png blank3.png blank4.png
khadgar 回答时间:2015-10-29 14:32:30
QianFan 发表于 2015-10-29 13:46; [) B- `9 P8 X& U- i, A& S
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
2 d; X- B3 ^4 I
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?
Paderboy 回答时间:2015-10-28 22:58:39
沙发啊。。。这个很有arduino的味道啊。。。
yanhaijian 回答时间:2015-10-29 08:36:02
环形队列很有用。
aabird 回答时间:2015-10-29 08:40:17
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
埃斯提爱慕 回答时间:2015-10-29 10:20:44
提示: 作者被禁止或删除 内容自动屏蔽
khadgar 回答时间:2015-10-29 10:49:01
Mandelbrot_Set 发表于 2015-10-29 09:08) h( W' y( ]: r) Z$ ~/ X
感觉楼主的 rb->count 可能不是安全的.
1 f9 p$ h- t% a& l- d2 Q$ i考虑:
" Q% M" T9 e) @7 |! Grb->count--执行到一半,

' h: o; E7 k0 I# m" [4 h$ J' a# U  k全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:40
6 ~1 x0 {5 J& e7 j哇,真的是大神呀,这篇帖子,说实话90%没看懂呀

& T! S7 ?, H, q) a用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:58
* [9 {. ~: b8 H* R$ O) L沙发啊。。。这个很有arduino的味道啊。。。
. \6 D( c( K1 f# h
一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:58
. }/ k. @; a. r沙发啊。。。这个很有arduino的味道啊。。。
7 k2 Q7 H/ L6 k# ~9 j4 k
只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:495 p; X  m/ [7 ~( @# J
全局量要加互斥锁的吧
6 E2 i7 D1 f8 D. M9 b& [" N
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:32
/ {; r  }6 f4 d刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?

, y$ |- i" P/ d( y/ hhttp://www.cnblogs.com/l00l/p/4115001.html
12下一页
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版