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

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

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑
3 u* `! K9 Y1 |8 Q" J' B9 |  b
+ ?: b/ X* L, }9 N+ P6 M7 N  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。
' R# P4 T' x7 w- E
- o$ _9 l% L) l* u/ ]. E  {3 [( y使用ringbuffer, p+ C9 b! v2 t( E7 N4 ~
这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。
) ^1 v! ?$ R9 J/ r9 ostruct ringbuffer
, `7 S* r  g! ~9 V0 g{
1 R9 F6 C; r& f) Q% Z  \& b% _1 @    uint8_t *bf;/ Q3 [! k. \, s- ~8 I
    int len;
' [5 K& ?2 }) r7 ?7 k: e    : }& j/ ^5 J# t2 L: d9 N: ?: U
    int count;" m& V+ G6 H/ Z. J0 Y
    int putidx; /* read index and put index */
1 g* j: f+ k" K9 `* [6 i7 E, W    int getidx;
2 T5 y$ B/ l$ K};
0 V$ j$ h- U% Y/ W1 ?
struct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。
( @( Z# B) u: E! u" q#define DECLARE_RB(name,len) \; Q" n2 k3 P) s* g5 Z5 `! ^
    uint8_t name##_buff[len]; \
' u7 w) K9 s. C, c8 i) O; K% C    struct ringbuffer name = { \4 G- R* N* |; D0 z
        name##_buff,len,0,0,0 }
! v) u( ~9 h# u: E4 J$ q6 _
提供的接口函数:
2 \: x& G0 A# }" D) C#define rbcount(prb) ((prb)->count) 3 G$ M7 l2 b, ^* \  g' Q
#define rbfull(prb) ( (prb)->count==(rb)->len )
. p! C2 N/ i  ^. T#define rbempty(prb) ( (prb)->count==0 )* l/ i! C% U6 L. I. w6 p" z

9 a) }. ^1 E. J: Z& y+ p/* if ringbuffer is full,return 0.else return 1 */
# h+ x, S( c% `2 n) U, `6 v0 bint rbput(struct ringbuffer *prb,uint8_t add);
. w$ c7 C. _4 V9 l/* if ringbuffer is empty,return -1.else return bf[tail] */
& t9 o1 b& U1 l1 C% j9 Nint rbget(struct ringbuffer *prb);
/ E5 h+ U! s" w6 O$ {7 f$ P5 P3 Q

2 f: Q8 ]6 T) w  P8 E( j; ?: xrbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:  g: `, K; B1 H7 i' x
#include "ringbuffer.h"' y$ e" A! |# k. W* e

6 }: s7 \4 A, ^6 Q+ K" |int rbput(struct ringbuffer *rb,uint8_t add)( t: d+ I; E7 M# j- {9 v
{0 K9 Z$ i+ P: d2 \! Y; w
    int curidx=rb->putidx;
4 b5 t& B/ v" h& `4 `    if(rbfull(rb)) return 0;$ T. h- G3 D; k' W
    rb->bf[curidx]=add;( K- D9 i! E  |7 C" ]4 P
    rb->putidx=(curidx+1)%rb->len;6 V" Q/ L1 v6 r) d
    rb->count++;
+ Q& \; q2 x8 Q# }- N& V1 V    return 1;  y; v& q0 C4 t- x' o9 s& S1 U: X
}/ \, q6 R' }5 ?9 F5 ]

' J* }' [4 q& x. P4 S, mint rbget(struct ringbuffer *rb)' Y0 x+ N  M9 Q& }
{5 \  F% b3 I& U2 i0 z
    int result;* G+ D) V' D1 \$ l. p; p
    int curidx=rb->getidx;
: S4 D) |8 y' c5 {% }3 J4 g9 Q    if(rbempty(rb)) return -1;
/ d6 E8 ~6 ~5 |2 y% P) }    result=rb->bf[curidx];
% K, E0 H# i. L3 y6 Q    rb->getidx=(curidx+1)%rb->len;
& i% y  P; J; H$ `    rb->count--;
9 I) H3 [1 n, B: d    return result;
& e4 _- U; M. o1 m5 g; T}% Z5 g7 h. x7 t* D% g

: J$ x# M8 {8 e" }9 U串口中断+ringbuffer  w5 c/ E0 r! T
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。& q  j4 B$ U- X2 G' r5 b
/* USART interrupt handler */
) a; t' j* U" E$ K) h, @" z# pUSART_Handler(); q5 o: U% a: [7 ]( t7 \2 ?
{8 U/ k! D# p, `& o  }
    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){
, M% e1 Y* M' Y/ a        rbput(&rb_usart,USART_ReceiveData(USART));: t) V, b! [. l: d* a  n
    }
3 r  m$ F1 a! h" T) Y' W+ K7 [}

; N7 {, h$ ^* n) a4 @, b# J, x+ d编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。1 A2 v6 m- d. p3 r
DECLARE_RB(rb_usart,USART_RD_BUF_LEN);
0 t9 o0 J# L: ?' O2 P% P- T
& d4 X" z" |, d$ j7 a5 j1 Fint __io_putchar(int ch)
( e6 b- }6 [9 W{
% G, l1 r: E7 A7 _+ f6 _    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
# I* ]9 v6 z& s) [# k* B# U    USART_SendData(USART,ch);
, X7 Q" J2 N! U, K    return ch;' h) i6 C( H6 \( S
}
) C" [. I1 W7 ^! q% K+ v2 M
8 V2 }9 n3 ^+ X$ ~  o' Z1 x3 q- Yint __io_getchar(void)
7 V% R/ @! l5 g& O- S- ?{1 }, J% T2 R! G9 X
    signed char ch;
1 m! Q" \) E. V2 ^$ @2 B& k6 B    while(rb_usart.count==0) continue;
  B1 p, i0 d' ]- d    ch=rbget(&rb_usart);3 q6 m2 [3 `3 {- t# s3 e: C
    switch(ch), G' ~0 L- k+ A/ l: g. g: l. X) \- D$ o
    {
. ]+ d- _9 H* c: E2 {" G; {/ G$ L        case '\r':ch='\n';break;
1 {. n2 N* k4 n        case 0x1b:ch=' ';break;
8 L+ t0 D$ M  k( i        default: break;
3 [' c; q9 e- c' L3 m    }' r  ^( J8 u. [! ?" \) g* i
   
: A9 Z9 P; w% q7 w  t3 N    /* echo it *// u0 `3 a. }) ~+ _2 l9 ^4 y( s6 ]% j
    return __io_putchar(ch);% M' L6 V% H' u# o/ [% ~8 R& r7 T
}
; m" T/ W8 N$ A& T- E
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。6 v+ l& H5 k: Y* R1 R/ _0 }, ^
由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。
  S/ Y0 s) |% i* w3 c1 r" ^
: A! t- w( i; z" o( Q在主函数中一直读取字符) f2 |9 p% q0 p/ _6 c. _
我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。
# [) ^! \0 s5 U, J9 kint main(void)
! x" V# G; u, J0 v; y& R7 V6 k{
# S3 |" \4 [# v* D: F    const char *str="Try to enter something...\n";9 a) U- N8 m; i" `: k; I
   
( S) ?" k. ]2 J/ i8 ~! Y9 Q    const char *tmp=str;( ^3 E5 x/ Q4 `; Z. L
    for(;*tmp!='\0';tmp++)
! G! h6 P' f+ a$ |1 z        __io_putchar(*tmp);
9 Q( r+ }* V; j5 e" K6 c        
; c5 H8 j+ s' T2 m2 }9 d7 h    while(1) __io_getchar();: ]8 I6 t. ?: g
}1 H  i+ t4 \' t. z( h: G
* R% x, \$ S! I/ S+ V
void _exit(int status)2 [" \4 c! w. E- Z# {
{2 y) b* [9 ?! y9 N; t; c
    const char *exit_str="GoodBye!\n";
' |  G/ K8 W, h& F    const char *tmp=exit_str;
8 x" r- \7 h( j( O    for(;*tmp!='\0';tmp++)
, I' A$ S$ ~8 t. h2 h6 B        __io_putchar(*tmp);" B0 m4 i7 Y9 I
   
8 l9 d7 e: @) e    for(;;) continue;- r, V4 C* `9 r; V8 r/ e" H# b, q6 F% U
}

) @. H* K0 M5 l# K; ^
: r, {7 }$ T4 j下载到FLASH中运行- {5 ~& Z8 d3 ]. P$ a2 S
目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。
* J" `7 [, y2 Y1 r! g! v试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns ./ v! p( c' ]: b# f5 O* {; y
不管什么原因,至少能够显示也是好的。
4 B' w& v4 t7 ~9 R3 E' q; h! x. Q& X" J' u
使用gdb调试代码, R1 ^2 J+ k. s2 q
新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。
$ A: G) l1 v; A2 S" k- ^ 2015-10-28 21:47:50屏幕截图.png
* S* y' n8 R' k0 g% H  `在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
! u) V- U/ f8 w2 C' b* X: }在gdb串口使用tar连接远程终端。如下图所示:! @0 k. O* z% k2 y8 Q% ^
2015-10-28 21:03:36屏幕截图.png
# T! y; z  D9 }4 C) B6 e7 Itar连接完毕之后,使用load命令将代码下载至单片机内。
+ W. |, K9 L4 C# `! g2 A; w2 e+ H$ e- E
至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。: G5 _2 C9 C! E" {" S
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
    $ B8 d  Q  e( v$ c7 }( p
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。
7 N, s5 y6 w7 u& @: R 2015-10-28 21:11:53屏幕截图.png
0 Y6 e0 j& S, y) l0 A在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。
  U: ~9 C& c+ c) f
; s0 A1 r- v% ^5 N 2015-10-28 21:15:25屏幕截图.png 9 l! V1 }: Y3 i
在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。
( L3 _2 a0 x  u( M: {; Y 2015-10-28 21:16:33屏幕截图.png " q, R' ^; g7 l0 z. G
2015-10-28 21:17:02屏幕截图.png : M- S, y  N0 r7 T% a* p8 B! k
在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。
: t8 @9 ]: _! O# L$ ] 2015-10-28 21:18:21屏幕截图.png : D2 G1 L$ m2 ^. p
在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)
# C6 k& Q) N* L" n% z当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)
; Q, K0 g' a9 i# I6 [" f! n" S" G2 F# `0 i* ~& L5 z0 C* N7 ~
2015-10-28 21:22:46屏幕截图.png
9 q5 w9 A! H# }2 O) Z2 ]2 X我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。+ Z" Q+ |" V. L9 Z: A
2015-10-28 21:26:09屏幕截图.png
  k9 X3 ^; e$ Q/ v使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。
3 T- B  {! F1 w1 _+ e7 x5 S. C9 P. [) r
如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。) w( r0 i2 f6 o/ Q# w$ S
: R: \/ B1 V! k7 C7 D" s

- o* l+ k- Q7 P* @6 a
; S6 [( B" w4 T
bug1+ z5 u" F3 y$ r& ?8 o  l" w
如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。
0 T8 R- o2 {0 i& j) M8 ~重新make all,make burn测试。' \' |2 k1 }# u1 ^$ O
2015-10-28 21:32:23屏幕截图.png
/ L5 A; g8 \: z, ~6 O问题已经成功解决。更正之后的代码在serial_v2.zip
6 E* s' o! H! S0 K& o( X8 s* l
5 t+ E4 C  {3 G* Ebug2

/ c& w* V& }' |6 x  Zbug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。
" q3 v" Z! B# A. Y) B# _! r一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。: f; Z) G! S7 ]; g2 x8 V
在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:* i8 X$ f+ c) @
#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )
  l* D- P. ]. P2 T#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )
% D0 h- i) ~. u; k4 u
我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。
' E+ g$ ~, P( r, L/ c& T$ i8 C8 G更新之后的两个实现代码:( {4 P2 f* i4 m# Q, [7 f' R
int rbput(struct ringbuffer *rb,uint8_t add)
; x! \( x, t4 p{2 l; K1 i0 M: K
    if(rbfull(rb)) return 0;0 E( d0 G6 r: |$ O9 }1 N; m
    rb->bf[rb->putidx]=add;( k( `: V( v4 d/ J
    rb->putidx=(rb->putidx+1)%rb->len;6 G: i& y8 C2 e8 }# v
    return 1;
7 e- S3 [$ v' B" W}9 w5 i/ l' m- ?' Z

# {' M* ?  G0 b5 o- Y- ^# Z9 `int rbget(struct ringbuffer *rb)
# U3 e0 C7 H$ z2 K7 y. ]" g# r{
1 j8 K4 }" k6 I5 b0 V8 L) w+ S    int result;
+ _% y" v% M# O. N    if(rbempty(rb)) return -1;: u9 t/ ^$ H: t
    result=rb->bf[rb->getidx];
# p1 \; q9 Q' I( W6 A    rb->getidx=(rb->getidx+1)%rb->len;4 `; u" s4 e9 D& x
    return result;# W+ v; [* U# \- B; J! L  u2 L
}! a. b# k9 n2 c  _
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断
2 E- j2 [, k& W  ~- \9 tint __io_getchar(void)( [4 e$ D" m9 T  p  O
{
. b( h* \# v4 {  ]- P) l    signed char ch;
" ?! `7 C3 N, d2 `' e+ m    while(rbempty(&rb_usart)) continue;- E' y! y+ d# E' e0 @. `
    ch=rbget(&rb_usart);
) z+ o7 Q1 d3 K% x% M; f    switch(ch)$ r  @0 ~/ `& h3 r, G  }: j
    {
8 g: Q- t4 ^: Y5 I( ^( J3 K( `        case '\r':ch='\n';break;2 Y/ k' F9 \2 Z' x% \+ a
        case 0x1b:ch=' ';break;) l( t/ }0 m/ p" p  Q0 R0 j# g
        default: break;
  p* ~9 [/ b1 }    }! H5 E; {7 J+ C4 R/ o
    7 S- G( A2 I, x. Y3 l
    /* echo it */
( E$ U. Q  U5 z2 G, o    return __io_putchar(ch);
& D. p- g; C. @6 ^, ^3 S5 R2 S- X}
" q- a- D+ n, u1 l9 _7 \
更新之后的代码在附件serial_v3.zip中。, s5 z8 M# G9 d* X

" Z$ R; U+ R/ r4 ?. C8 c
/ S, `/ {; M4 }+ @5 z  T$ V9 r6 T更多. N  W- b5 W8 @/ Q. _
在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题

0 p% N; d+ _) m% I! Q' r8 q* d& \2 |# n, y) t% ]% O! \1 L
2 ?7 ~$ W# U, _1 }3 M9 H3 t
# V' y4 x; J6 V# P7 Y0 @% T& b5 z# c) `

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 可能不是安全的.
, u4 @4 ?" n9 I' R( d: l4 {3 k0 U考虑:! \8 j, E+ @0 ?  |
rb->count--执行到一半,' o2 ^$ g% ~8 N- M
此时进入中断rbput里rb->count++1 d. ]* ~/ n: E4 `& r
可能会导致rb->count计算不对8 ^6 S$ j8 k4 v* O
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:084 @. }9 @: ~& p# [
感觉楼主的 rb->count 可能不是安全的." K+ f3 D9 d5 B
考虑:
# u1 o# q, o* M( ]rb->count--执行到一半,

9 x+ _+ g* L- ~2 n9 u确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。9 G" |# Z6 p1 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# }9 [2 D  ~4 K9 \' B* q, Q& [
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
7 N) ~$ F1 Q! _7 }! a& v2 m/ c
刚开始接触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
# c6 i- E& H# g. ^1 k. c感觉楼主的 rb->count 可能不是安全的.
5 F0 }! `* {/ m9 R5 G) W9 Q考虑:% `4 L, @! x+ `$ _
rb->count--执行到一半,
9 V1 l3 c" C3 h4 I% o
全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:404 P& B7 l) s! u( F' T; Y4 @
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀

/ s- T  p; ^8 O) B$ L0 r用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:58
) _+ G% N3 @) l! r. }. `' e2 J9 A沙发啊。。。这个很有arduino的味道啊。。。

7 c3 W! b7 T+ B0 e) I. \: e0 \一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:58! _- b& y) F7 D- ~( l; W$ R
沙发啊。。。这个很有arduino的味道啊。。。

) P% m/ B2 ^% {% i& q只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:49
2 y2 ?# W- f! Q" L- {& r全局量要加互斥锁的吧
5 T6 N, y; T' Y- ?6 l+ f! Y
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:32
) r& E$ G8 Q  y8 H' p; p刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?

( ^: n# U( x: nhttp://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

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