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

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

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑
! p* `$ ?0 |8 N  V* V0 _$ g/ z  j! E' e6 Y5 s$ i
  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。5 n" b1 x! }# o+ s% X! i
0 o* ~2 W4 ~7 ^& x. P
使用ringbuffer
& B' R3 G3 U! G这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。
; V9 O) E& }  R! G) @struct ringbuffer
0 \6 j5 Y5 V$ Q9 j+ ~7 @{. @4 N: ^( i, S0 e& w0 H
    uint8_t *bf;
% I" F' Q$ ]: k) Z# T    int len;( d6 a1 s+ L, E6 Y1 }% l
    ; a: `3 c) g/ Y, @2 a  T
    int count;- P9 n+ w& X( y3 K
    int putidx; /* read index and put index */
: i# w8 k/ G3 l( e8 [: N+ b    int getidx;
9 |; _- \7 u. H+ X6 y};
, `2 U0 |" ^/ X) Y3 ?
struct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。
1 m; j4 T# S/ ?2 U: _/ z# J#define DECLARE_RB(name,len) \
. y7 ^0 u# m/ |" b. W1 @    uint8_t name##_buff[len]; \
/ e0 x$ [2 N$ e    struct ringbuffer name = { \
. z5 h  S6 [2 r+ G& q7 e        name##_buff,len,0,0,0 }

& r* x, G3 z5 V4 _  e# N提供的接口函数:
7 G6 c# d$ B) g+ o0 s+ V) {0 r#define rbcount(prb) ((prb)->count)   J$ F7 |- p5 [' U! e# V( s5 S, F6 b
#define rbfull(prb) ( (prb)->count==(rb)->len )
  @) Z  f% q; m6 E: \* s. r#define rbempty(prb) ( (prb)->count==0 )" p7 _& D+ S6 B* K# t: C* K* W

, w( h2 t3 j& z1 l. q+ X/* if ringbuffer is full,return 0.else return 1 *// @1 \: ?# t2 R. C. j- ?" K  B# B
int rbput(struct ringbuffer *prb,uint8_t add);/ t; }' x4 Z! g3 n% x  H) f% p  B
/* if ringbuffer is empty,return -1.else return bf[tail] */! b& I/ E* x7 A9 O
int rbget(struct ringbuffer *prb);

) x3 j1 t) a* |/ j1 h, U8 |8 Y2 v- ?) q# }( H
rbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
* Q" R$ G, L: M; W# }#include "ringbuffer.h"
0 a& ~3 u5 p$ Y1 C1 q7 X  ~4 ]5 {
) S1 J4 C" ]9 e, x/ ]int rbput(struct ringbuffer *rb,uint8_t add)1 ?/ p5 B% D3 d$ L; s
{
$ A" \* o3 `/ `: A6 T  ~, y    int curidx=rb->putidx;6 b# O9 F& a! u
    if(rbfull(rb)) return 0;
/ Q5 z! k" q. f    rb->bf[curidx]=add;& ^4 \% R' x' D: Y% z9 u
    rb->putidx=(curidx+1)%rb->len;! t7 j% H8 I3 u& t
    rb->count++;
. e  j* k5 \, \, A4 Y; z    return 1;5 \9 U- l" l2 ?$ {7 w6 R/ y
}
4 p* _. @; n4 V$ S5 x1 D2 H
& b  N5 w2 Q! P% P6 Wint rbget(struct ringbuffer *rb)
" P! M+ C' E8 V; y{3 A! r9 T  c) W6 p" Z$ ^: m0 M
    int result;
) X1 o0 U" {, I0 a" M    int curidx=rb->getidx;7 E* n, R/ Q, ?9 e% |
    if(rbempty(rb)) return -1;2 M# D  {  Q: Q
    result=rb->bf[curidx];
1 A% |+ \  @; i' P    rb->getidx=(curidx+1)%rb->len;
, d2 e7 L" y7 @$ d2 O    rb->count--;
1 r. N& F3 Z! E    return result;
, K3 E7 Y% z8 ?1 l7 I. J" f+ [}
* F  F& q) ^: m' j

, Y. \" S5 J7 p# @0 \串口中断+ringbuffer6 R3 p2 i' z  G1 G) h
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。
( v' F  _2 F! ]$ Y/* USART interrupt handler *// L: r" }6 B, A" s3 _4 A
USART_Handler()7 S6 ?) Y7 C5 d  q3 g1 \0 J
{/ z3 `0 F% W6 {+ p* G6 T& x
    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){
- x" N1 T( K# J, R4 ]) S        rbput(&rb_usart,USART_ReceiveData(USART));
5 X0 b5 N# c% [# V+ j- v" A7 {( D6 B    }
6 `3 z0 t# I- U1 O5 B# V5 d}

: t$ l, M( j4 c7 J$ ?  y; }+ l编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。
5 t) c' q! F; H( u5 a% |9 bDECLARE_RB(rb_usart,USART_RD_BUF_LEN);
9 Q& L( o: X7 B8 Q. X5 N5 R( ?( E  I8 Q: s: E2 v3 b
int __io_putchar(int ch)$ D# L2 M# I8 v5 }" c2 c7 n& {
{8 |: c0 y8 x, u1 m/ i
    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
: p4 b$ a4 |! V    USART_SendData(USART,ch);
5 C4 U% \. S5 X, q5 ~    return ch;
$ U& j! W9 u" Z" F) E}% T0 S' H% D9 f
$ i$ J$ r, X9 O/ A& e
int __io_getchar(void)0 B( }2 q8 V1 W- q
{
4 k1 o( {, q7 c- Z; o    signed char ch;
, T2 ?5 V/ ^1 |1 s& h, \" o$ t# \    while(rb_usart.count==0) continue;2 V; d: J% S5 T4 H7 v! k/ @
    ch=rbget(&rb_usart);: F# R( r) A1 Q: ~" Y2 z
    switch(ch)
( k: |/ ^- D  Z. J5 O$ p+ \    {
0 H' X, O. v2 {. _        case '\r':ch='\n';break;0 h+ h4 m7 r0 \
        case 0x1b:ch=' ';break;! J! _4 ?: {8 e9 j$ d- a
        default: break;
- N  u; d# k' Q    }
: `6 B, N8 u; G. g* y- v/ \   
: X5 c; E' N7 m    /* echo it *// A- J, w( |0 Q. F
    return __io_putchar(ch);
! i( D4 y4 u9 M6 r& S' D) S. s}
1 z9 T6 E0 N6 H4 s
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。
; A1 B4 A8 M4 n, \5 Y( C由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。5 u! c+ j5 q4 @* h, W6 p; H

3 w$ A1 l! \4 b( n/ Y( o% C6 d7 F在主函数中一直读取字符
7 L; J- ]8 j& Y) r6 u. _, O我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。
" O/ `: a$ H# kint main(void)1 Y" }4 J) z& ~, N2 _0 F+ n, b# F
{8 b0 s8 |, _3 q% }' Y5 [
    const char *str="Try to enter something...\n";
! C7 ]+ N) ~. o% }" _% v1 H    2 F: y: i$ W3 g( k
    const char *tmp=str;
" O9 [7 R/ y! t! s- R    for(;*tmp!='\0';tmp++)* ~3 Y1 P9 E. x1 ?  U
        __io_putchar(*tmp);
+ f( l9 e: ]7 y        # ?5 E9 ?- \. G3 G  u. C
    while(1) __io_getchar();
5 K5 x  b# z  w$ l2 y. J2 f$ g}
) O- J: K) |! e5 K9 D2 H' ~  M; v& ^) P  S, f
void _exit(int status)# F4 Y; x1 s$ ?' I3 E- `% l
{
5 J/ k6 c! Z0 j9 s$ d# ?7 H7 @    const char *exit_str="GoodBye!\n";
' V' E  g& C. n& P$ |+ S    const char *tmp=exit_str;: l0 h0 U+ l4 w/ W0 s  a" S. ^
    for(;*tmp!='\0';tmp++)3 B) O7 f, `" ~6 M4 }- m
        __io_putchar(*tmp);9 D* E" W0 f* {0 w/ ?' I- X' Y
    ; Z9 D2 z' v5 m2 s5 I
    for(;;) continue;
1 X- ~9 S& l, }7 a& y. ?+ y/ f  k}
0 {* n' f9 D& W8 Z2 E! a
' i$ w: {. S8 Z& X+ i
下载到FLASH中运行5 g" K( k9 o$ C' u. e
目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。  I7 N. i4 B7 Q1 K+ C2 T2 o
试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .
8 U' `6 L& h' g% [9 h. q+ U" g1 ?- }不管什么原因,至少能够显示也是好的。
( y. U/ ]: f* P! A/ ]; _' Z- n0 O% i% p+ H; _5 R0 O5 Q6 q/ W" U' ~
使用gdb调试代码5 E6 k& M' M. m, b: j# y
新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。
+ d1 T( P. L, m 2015-10-28 21:47:50屏幕截图.png 5 g1 r5 O0 p# D3 ?& m) K
在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
+ ]$ M. X) s+ P- x! n( F  _5 [3 q在gdb串口使用tar连接远程终端。如下图所示:: y! o  r) K+ ?4 h! t
2015-10-28 21:03:36屏幕截图.png
8 E7 F0 U' C4 G2 U, l8 ltar连接完毕之后,使用load命令将代码下载至单片机内。
. l1 Z. s% K* ^/ b# g# [1 H( b$ _6 _- O% Z
至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。
) Z3 S& f; ?- t' g" V
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
    ' G1 F8 q! c3 D9 u
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。3 ~6 ]- s2 n+ f& |0 p! C
2015-10-28 21:11:53屏幕截图.png
% n8 k- I2 O* x" B5 i1 ^; O在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。2 Y# d8 H' Z5 I" `( ^

. @' c8 V) _1 u% k 2015-10-28 21:15:25屏幕截图.png
8 M. t( x5 |6 L# `1 o  E在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。, P2 E+ i3 q" D" C1 j1 b
2015-10-28 21:16:33屏幕截图.png
& w" V5 b0 @5 {) D! _9 B& C7 b& | 2015-10-28 21:17:02屏幕截图.png
8 l" R1 N3 ]8 d  m  u, \在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。
! ]' T) e- h9 T. [- Y 2015-10-28 21:18:21屏幕截图.png - j0 K4 y0 i# x0 ]
在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)6 q# k9 |. W- v
当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)
$ ?3 ^& [, X. E# K$ Z7 F9 U. ^2 [& s' h) r$ O  S1 U( K. ~
2015-10-28 21:22:46屏幕截图.png
) j1 L$ r+ P( U* b- m. [2 A我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。& _2 S' t  }3 ]: R  q+ e  V, ?
2015-10-28 21:26:09屏幕截图.png 9 D6 y( \6 W- |! K
使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。. ^% S& p3 N1 q1 s7 Z' a& U5 e5 D
. D4 `( B% S: M. y* J* I/ f, U1 X
如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。, V0 `; d5 C) R2 S4 {& d+ h
) ^+ g/ {6 s0 |- Y8 m, Q

# \; u: G3 O0 P9 i* u. L3 g

8 f7 B. Y$ M) e" S3 H5 f0 ~6 q, n1 gbug1
" n9 T- F0 u! B如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。8 n& ]+ w* o. `# [. {
重新make all,make burn测试。
7 s: T' z, f, u 2015-10-28 21:32:23屏幕截图.png
! E7 v, n( j4 G% [9 p问题已经成功解决。更正之后的代码在serial_v2.zip
6 v3 i4 h0 |  P+ U# X2 }& }/ }  N- D
bug2

# ]1 U% ]# f  E2 O" }9 gbug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。" B$ w! N6 c! p) y( ~9 A8 @
一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。
) i) }2 A; R0 g. k5 b& A9 q在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:; }" T& T1 M$ l
#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx ). v0 K/ ~4 J5 C* x2 H
#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )

/ J" v+ c- g) W! T0 f我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。
; {& l: X3 c; a* N! E更新之后的两个实现代码:& I) ?; C: l$ A. s  @8 R; S" i
int rbput(struct ringbuffer *rb,uint8_t add)
# r0 Q0 p& Q, S5 d8 h* I* q, S4 y{
" e! v. N  p9 G6 \! C4 I  S  K    if(rbfull(rb)) return 0;
% {6 f+ h9 L2 u# s    rb->bf[rb->putidx]=add;& m. `; r5 v) K0 N0 X$ r* y1 `
    rb->putidx=(rb->putidx+1)%rb->len;
+ ]; ?) a3 B7 f9 U. E8 S' C. M    return 1;) E: M5 B0 T( E  W
}! d# D4 ?- `+ I- K4 l5 H

/ s5 R# ?) G0 A$ n$ C4 hint rbget(struct ringbuffer *rb)
' E6 V/ ~9 t5 @( c{  j9 `. [" A# @& @: j2 I
    int result;
- m+ N; \2 h. ~9 Z% q! B    if(rbempty(rb)) return -1;
& X0 j4 Y1 I7 i. C$ k    result=rb->bf[rb->getidx];
; H" v& r+ J' O7 u& F  I8 w6 J3 C- z    rb->getidx=(rb->getidx+1)%rb->len;6 P  C& t( l3 r) T6 m9 H
    return result;1 z8 q* A4 _9 a/ ]! e2 X, Z
}
% o1 l% ?+ r3 ^+ K- B2 `
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断  {( E: q% P3 F6 u* c0 G4 f  |
int __io_getchar(void)
% N8 @5 V. S. R+ V5 x{- k+ p' t0 z& N- Z) y) U
    signed char ch;. r" k- L! K1 T( O6 L7 q
    while(rbempty(&rb_usart)) continue;, `1 I/ h8 y" h" [
    ch=rbget(&rb_usart);
7 |: y0 L. ]2 X4 o$ y    switch(ch)
! a6 I* o& L8 z* T- R) _) O    {
; H6 n' [0 D6 i4 J' }  P/ [0 O  k! M        case '\r':ch='\n';break;1 B9 c, C  N& O' Y+ a# X% N' w# H
        case 0x1b:ch=' ';break;
; {: o* @) r$ B0 W        default: break;
( W: O8 P# I+ ]$ p8 T) |) R    }' w2 f- {7 A* [2 H
   
1 p( o, H* X6 B' U3 m1 ~    /* echo it */( y) W; s- h# k" \" j
    return __io_putchar(ch);
3 _8 F2 K* F  ~- A0 l, i; a+ e}7 }2 j: H+ j& d3 {* _% t
更新之后的代码在附件serial_v3.zip中。; I, }( ^  t: O' N, P

& E( s/ y/ h! Z/ J- b& P8 W8 r2 v( u$ `8 ^0 C# a0 _
更多& l% H% o; i+ Z: f
在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题
+ m5 @1 F! B' L5 j7 O
& x5 H4 U! x0 |) r

9 P& u- @+ b8 u# ?9 U

* |% [  ?( c8 O) j

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 可能不是安全的.- i$ B8 C. G' V% J
考虑:" |) ~5 t3 @7 N* k8 \
rb->count--执行到一半,
/ N) I6 n; m, m& O此时进入中断rbput里rb->count++
+ W" u7 u* ?# t6 A2 t可能会导致rb->count计算不对. S0 X) F/ c% h  u9 }4 F$ I
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:08
$ r# d( J7 }2 I3 N0 l5 q/ Y感觉楼主的 rb->count 可能不是安全的.  N7 t3 F) [, D: Z% p( T- H
考虑:
& b8 R  _/ I4 Orb->count--执行到一半,

& `( m5 ~4 L" _( T  Y. a确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。
: `  u' m, G: ~9 E还需要改改。
党国特派员 回答时间: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' S1 O( ]0 F, D
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
: L0 N2 y$ T  l! v+ V
刚开始接触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
6 Z( {$ |5 u8 p) R感觉楼主的 rb->count 可能不是安全的.. [0 u  X( @+ B2 ], F% ~
考虑:
8 k2 b- b! C& `( Xrb->count--执行到一半,

3 G: O) w* @* q% @3 Z全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:402 t# u; t# }& _; F$ M8 y
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
0 `% \3 V2 F( B6 ?" I
用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:58
4 ~  ?# s' @/ [& _' [' O* I沙发啊。。。这个很有arduino的味道啊。。。

9 f. B2 ?3 E& X% F$ Z7 d一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:586 o1 g5 ~9 ?; y
沙发啊。。。这个很有arduino的味道啊。。。

# n: f" s4 L) m: v' a/ Z7 N4 g只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:495 S( a$ L2 P& W/ U7 U
全局量要加互斥锁的吧
9 D6 p3 D# i/ w- C
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:325 I3 s7 k" z0 \, a+ g- @% B& K
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?
1 `0 s+ j! h$ }: {  {+ k
http://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

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