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

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

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑 5 a# y3 P1 j' _7 W  T( e
/ k/ ^5 V0 Z9 V
  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。% E8 \" N3 W8 t
2 v1 W: i, q* ^# T4 s
使用ringbuffer& g1 O3 I5 u5 b5 Z0 {$ H: Z
这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。
3 L0 G/ ]/ I8 Z3 |8 b* M2 xstruct ringbuffer& |( N- A" o4 Q4 }3 C) M6 s
{6 q1 E3 E; c/ {! d
    uint8_t *bf;  S0 x- z% m$ Z
    int len;
' |$ I% m# M' I( {. R: r    / |1 W8 u5 s- ]) ^; F
    int count;
5 z3 Q, q+ v  W: ]% g    int putidx; /* read index and put index */
9 s1 h! j) k$ `* C    int getidx;
) w( i* u8 F! p7 s5 e9 ?) A$ P};
! d4 k' `4 i. \% u/ x& i4 g% M
struct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。" a, e' J) o; l5 C( E/ {
#define DECLARE_RB(name,len) \2 |' r. j0 b) o0 r/ [
    uint8_t name##_buff[len]; \
1 ~- L. K% K2 P0 V: Z: ~* {    struct ringbuffer name = { \( U; }. n4 e" S
        name##_buff,len,0,0,0 }
' q- ~$ t; j( Z5 v: O
提供的接口函数:
  Z# ~5 Y" r; p; W! ^) _: s#define rbcount(prb) ((prb)->count)
( `, @1 U9 m+ j+ h#define rbfull(prb) ( (prb)->count==(rb)->len )# F$ y$ y1 E6 [- ?& L8 F
#define rbempty(prb) ( (prb)->count==0 )
1 |+ A% _: J7 e0 O) i1 f! O2 _9 v9 M) u' @. L7 B  X
/* if ringbuffer is full,return 0.else return 1 */
% X3 [) R. E3 Y0 D8 I8 Yint rbput(struct ringbuffer *prb,uint8_t add);
  K5 I1 q4 @+ e; T) \7 l* S/* if ringbuffer is empty,return -1.else return bf[tail] */
) R  W# W$ }+ K) [2 g3 Y* N% qint rbget(struct ringbuffer *prb);

. g+ x# w, R; U5 H. k, w- _- ], y; M& W# P5 n
rbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
* s' L3 X& D# k#include "ringbuffer.h"4 U0 c% R5 X& b$ _# M

; T. m: h3 N# C1 Iint rbput(struct ringbuffer *rb,uint8_t add)1 D+ u( q) ]% G. i5 P
{
; `" h- J5 U8 J! T    int curidx=rb->putidx;
5 l6 z( g" a; \( X9 i    if(rbfull(rb)) return 0;
0 j, v4 k; }8 a. c    rb->bf[curidx]=add;
2 O. k; |/ _0 Q    rb->putidx=(curidx+1)%rb->len;' g6 |$ A) s% g
    rb->count++;* A. V9 k+ O2 Q4 ?$ r
    return 1;1 B" [6 m6 h% P  n% a% h
}6 J6 {7 J7 U! G# d5 a3 z

4 Q$ f6 l* n+ g& _int rbget(struct ringbuffer *rb)5 L6 `  _- f. U2 d( Z+ t# @7 H* B
{
# Z7 |$ C$ O4 J( `" ?    int result;
2 E4 Z; |4 `; r0 V4 _    int curidx=rb->getidx;8 k8 W$ [  m- |4 M* S6 z
    if(rbempty(rb)) return -1;
( U# P3 }/ Q+ v1 g    result=rb->bf[curidx];$ l2 ^# V5 f# T$ o7 z
    rb->getidx=(curidx+1)%rb->len;
% X* P. A" Q- v! w    rb->count--;% ?/ {# r) V$ k, ]  _: ]* Y7 |
    return result;
% c! Y1 h' q- [! z( m) G}
+ L4 `7 f: ]7 F* C1 S5 W

: f% e1 h$ K! l# {串口中断+ringbuffer
8 M! O5 }6 S# \2 _( H% s
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。* ?$ U# k7 f- ?
/* USART interrupt handler */
, M  V2 J  B- F8 D. iUSART_Handler()5 B  S3 P! k& o4 Y: \
{
1 t: b* v' u. m- c    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){
) S- j' C( [( v; A) R; c- r( P        rbput(&rb_usart,USART_ReceiveData(USART));
) \( e: c1 x) o% j( S/ t* N    }
" S0 p* g8 q% b2 g' J}
" d; C5 K9 O- z+ @  @
编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。  `- e% V- e" F1 n6 b( l% Y0 G7 i
DECLARE_RB(rb_usart,USART_RD_BUF_LEN);+ Q. n" s3 q$ Z- X4 l* b7 y

4 Q+ \3 Z; g! ]; K, ?$ q9 oint __io_putchar(int ch)  H* K, |3 Y, X7 C% }- t5 h3 U
{# A  d! k! d; a' a2 g
    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
" K7 m0 Y7 j9 a5 h- I    USART_SendData(USART,ch);( h" j- r' G0 v( S" U
    return ch;
) {% W5 x8 @2 X1 M}) F. ?. l; ?: {  J+ p) u0 y8 Z4 J( L

1 {9 K; v; x9 `4 u) R- E: ]! S2 |9 mint __io_getchar(void)/ z: b+ _0 a) r0 N4 h( y
{1 ^' a# l- J  }5 t6 M: F
    signed char ch;6 q$ k7 I5 a8 M% t' v
    while(rb_usart.count==0) continue;: K5 \0 M- I+ y3 s+ C4 Q
    ch=rbget(&rb_usart);/ F1 P9 }5 j- U8 W2 q# w
    switch(ch)% x! o8 e% u4 j; _9 r
    {
4 L6 J' b; M8 |( m( z# o. @# S$ `        case '\r':ch='\n';break;
+ P( R3 t: B% W! ?* M/ O* L0 ^        case 0x1b:ch=' ';break;
; N- d1 d& s% ~  N        default: break;
7 Y8 _! s7 l8 Y( T. Y" E    }# w8 V1 Z. ]; N, g2 f# B
   
! s/ a; v1 ~; n0 J- K" f" c1 D+ _    /* echo it */4 P" s# e) L/ g/ o3 T- K8 |
    return __io_putchar(ch);0 |- E2 c5 Z8 |, K  B1 D+ Q
}

' e3 b# f, ~, U# ]9 W, H; K当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。4 f$ M* Y2 a% H5 y3 u: A  j6 D) ~
由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。. y' f% N! s4 y- h1 c. N
+ I2 q# ?4 e: ?4 C$ Q7 N! A
在主函数中一直读取字符
( Z% t0 L, c5 @4 ^0 m5 i我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。
4 q' g; D) F# F! k$ |7 P9 d# mint main(void)
5 O! d/ v' S* W9 t{
4 i: [/ K8 I, y. S, w& y9 |: z    const char *str="Try to enter something...\n";
2 }4 S1 Q& V  X% c8 ]   
0 }( j  J. V4 T! T    const char *tmp=str;/ y& {  O9 }) F8 O; J
    for(;*tmp!='\0';tmp++)& j, d% S& d* n, F9 V
        __io_putchar(*tmp);
4 Y7 D, T, |2 v7 B4 t        
0 X: L) A2 x4 g  b7 [1 }. J' {) X    while(1) __io_getchar();
* V0 d: _& C( W5 a+ ~9 O}- M' q+ z/ K9 J. Q: t
9 R# h+ B. e% K
void _exit(int status)( {* M( d7 V; T* m8 z/ |% D
{2 r2 W+ s+ e+ b
    const char *exit_str="GoodBye!\n";
* D. S* ~( N' W6 ]) x    const char *tmp=exit_str;
( l! t( `% a" n) w$ ?3 ^, V7 s    for(;*tmp!='\0';tmp++)
( M7 h" k9 z7 k9 K8 e+ C        __io_putchar(*tmp);
2 p8 G- a9 W* H" y3 ~    % L& E+ w0 L) V1 @
    for(;;) continue;
; N1 D, ]& H2 Q0 V* X* A4 ]5 R* U}
% L9 X3 h8 z' E3 a
9 n! K$ C) @! d0 s
下载到FLASH中运行
: B( s: l* V7 d% E: P) E6 p目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。. f. d; ?* O1 g4 d+ O0 Q
试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .7 P. h; \. Y" P
不管什么原因,至少能够显示也是好的。
+ g7 e# K/ D1 v# W' H7 Z- t3 o5 a  M4 b$ r1 ?9 ~  _2 w
使用gdb调试代码1 r  p" H8 {$ {1 S5 g" ^3 r
新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。
) Y$ p; t) m: u; E 2015-10-28 21:47:50屏幕截图.png 3 m- H; I/ b8 T
在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
6 O" Z% n) b2 G在gdb串口使用tar连接远程终端。如下图所示:
2 G) l/ H! Z1 }" C/ g' o8 W 2015-10-28 21:03:36屏幕截图.png
5 }' Q3 q, L. p, Star连接完毕之后,使用load命令将代码下载至单片机内。4 H. c/ e- `5 p' G# Z" O' I
: i) Y+ v2 S1 O9 C) O  ?3 y
至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。  \7 @" l. e$ D
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
    4 {& O/ y/ }" Y) [
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。8 k( Z2 J. D( w& u7 d
2015-10-28 21:11:53屏幕截图.png
8 f; s( O% N& z1 Z8 l: s5 _在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。4 T+ M. y2 \& p, V+ i

/ U3 K2 d5 `# l. N 2015-10-28 21:15:25屏幕截图.png ! O$ U1 L5 a6 p- e
在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。
, h. d; m, U* b# g9 x+ q7 J" {  p/ R) v 2015-10-28 21:16:33屏幕截图.png   @" Q, D7 i6 b2 q
2015-10-28 21:17:02屏幕截图.png 3 z' x; t8 F/ T& G
在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。# z/ n# N* b; o' g, Q
2015-10-28 21:18:21屏幕截图.png
) f* G+ ?& L& ^- M  R1 S在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)
6 K/ b0 K! O; Q当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)  C5 r! q7 E; ?
9 g3 e: z% x6 _- q; M9 f
2015-10-28 21:22:46屏幕截图.png ' h+ p" n% |9 o7 m; w9 R
我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。# Z% j2 \4 Q  t* i7 u& B2 D
2015-10-28 21:26:09屏幕截图.png 5 {& I! I5 q( m: ?& C: M; |( [8 b
使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。' a. b0 R7 K3 O. C
4 s1 o9 d+ i  I8 X  M1 e, ]
如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。
( b: C4 u8 {+ v  d6 Q* K; b, w3 r
; m# T, _. u3 t0 e; l% L, c+ M( i9 ~' N

3 |7 t" L& `; s. {  R5 g7 J6 ~bug1
4 O: @0 T9 C( U% ^如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。* ~+ G! f) K; w4 l8 x& B
重新make all,make burn测试。
5 q1 ?. E2 }6 h1 m  _2 F 2015-10-28 21:32:23屏幕截图.png 0 e! O5 F4 G0 I: Y- [$ ], u/ A, y
问题已经成功解决。更正之后的代码在serial_v2.zip
1 Y8 F* {* z- N1 X& M" k9 x5 o( T
1 C, Q1 |4 ?: ^5 l. ~8 V* Qbug2

! P! C. ^, ]5 U8 R0 h& s4 Q/ Kbug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。
9 m5 k& \  E6 o一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。
4 ^; h2 o! K3 I7 f在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:& W- b4 d- {4 c  T1 {! k
#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )
; Z: t; n, d: A" K#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )
8 N; p1 o) G8 Z; {( P' U" X+ C0 ]
我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。
9 F  E: l2 J; I) C0 c) L3 n更新之后的两个实现代码:, s$ h% s( V% ?' u
int rbput(struct ringbuffer *rb,uint8_t add)9 |6 t0 E% K' \/ Z- |7 e0 J
{' r8 z6 K8 t& k1 g/ ?% j! W
    if(rbfull(rb)) return 0;: ~: S- G7 c7 D% w
    rb->bf[rb->putidx]=add;. r3 l5 |' ^7 P& O$ J* U8 }
    rb->putidx=(rb->putidx+1)%rb->len;
" z8 f- _% M' p( ]  A) g    return 1;+ B% N1 j- ^! j4 x; w9 C
}
! p3 R* f# B2 |+ w0 C! d( T. U9 E5 ?* x" b# Y- W- r. Q
int rbget(struct ringbuffer *rb)7 P: m; ^' Q, w* B& i
{' g* ^0 d; \- }: r" A/ @& ~8 z9 g
    int result;0 w3 W7 G# I  t) j2 k; J6 f$ v
    if(rbempty(rb)) return -1;8 `. s6 ^0 `: u# `: n+ x( A$ I' ~
    result=rb->bf[rb->getidx];
" v" T# o' R) d' H% A- {9 C    rb->getidx=(rb->getidx+1)%rb->len;
9 i6 ^2 ]5 q( o- f2 V: E# f0 O: G    return result;' m3 W) u) U1 T, I/ }) \2 y
}- ~% G$ ?2 [' q  v
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断8 N5 Z" ~+ J2 _
int __io_getchar(void)0 w5 K  ?0 l9 [, N; N, b" [
{
  O6 _+ b, k0 ]' A6 E! Q* {9 B  r    signed char ch;6 c9 f% q4 c3 N: x( N
    while(rbempty(&rb_usart)) continue;& L+ ]- n  R1 h7 J4 f0 B
    ch=rbget(&rb_usart);
: q+ M' B5 z  D0 n    switch(ch)
# w) d+ O3 Z% m, t3 G- b& J    {
) g& A+ K1 `0 @: F  d- Y        case '\r':ch='\n';break;
" W0 s6 p1 L  [: X% v        case 0x1b:ch=' ';break;7 K9 t2 E8 a4 T0 R
        default: break;
& y$ t7 _# {7 V* Q& {+ {    }4 h, {* q9 S9 q' X0 v' A
    ; O4 N5 X1 E/ p
    /* echo it */
+ Z% f. v7 Z0 B  \3 G' ^/ K7 B    return __io_putchar(ch);7 e7 B$ s5 X2 X0 }' _6 D7 @
}3 r$ P( G/ i: ]3 P% G: |( _  N/ B; c) E6 {
更新之后的代码在附件serial_v3.zip中。
7 y; Y  Z- U0 b  B2 i# w- A3 ?- i1 _

4 S; V1 T7 I2 z. a! R更多% [& P' ~! E7 B9 i& w: y$ V
在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题
' a2 T  s3 s4 f
( h2 v  J$ U7 l
: @: `) F7 g0 ^# B$ g

0 Q. f$ |* {0 W! l8 \

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 可能不是安全的., x- E1 Y" M! W9 t6 s
考虑:
3 c4 c- m0 ?+ }8 Z% Arb->count--执行到一半,
3 B! D  _, Q5 D此时进入中断rbput里rb->count++' j  z9 K3 w' |0 N; F, U* Z
可能会导致rb->count计算不对, q' s0 D9 S; H" d! ]
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:08
- S, ?2 N6 s, J# a4 O& o5 \感觉楼主的 rb->count 可能不是安全的.# `4 k; ^1 D  ?% }( G/ v; ~3 {' O
考虑:
+ x* o, A9 d3 s: d) irb->count--执行到一半,

! B7 X% n8 R6 _确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。1 C4 h% T: v1 i8 N
还需要改改。
党国特派员 回答时间: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:466 s4 L, c4 j  P" X9 H' H6 u/ j
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。

9 I5 F# F# ]2 m! H4 B/ E3 P刚开始接触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) Q' G2 G+ c' _* P8 P
感觉楼主的 rb->count 可能不是安全的.. i) O8 k5 W3 p, Z
考虑:2 A, q! W' Z" ]! k5 d/ I; p  R- ^
rb->count--执行到一半,

6 z9 M8 D3 p7 k! Q全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:40
3 c. f1 L, {- w) O  o/ Q: c/ M5 @哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
) x, a$ ]) s% x5 o6 K7 p
用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:582 N9 n* k+ z$ W) V
沙发啊。。。这个很有arduino的味道啊。。。
+ C9 S6 q% n8 N0 ~) G+ E
一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:588 I# x1 i' ]* M) C
沙发啊。。。这个很有arduino的味道啊。。。
; I0 L# w+ p6 t& j; F  W
只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:49) {( d; f8 w$ M, m, N& v
全局量要加互斥锁的吧

5 a' _0 Y" a' h8 }8 A( K* B把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:321 ^+ b6 G0 {- Z2 Z1 q
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?

; v/ L8 V+ M+ V* ~http://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版