本文是STM32U0测评系列的最后一篇。我认为STM32U0最吸引我的就是他极致的低功耗,ST官方也说他非常很适用于水表等产品的应用,于是我只做了一个电子墨水屏RTC日历。% K% `3 E y, r& Q+ f& z+ {5 v( K# T
& R( q0 Z! f! {) K% q& `: o" U, p% a O* v, b- U$ h
一开始我是想做日历+时钟+温湿度。但是很可惜我手上的这块电子墨水屏不能局部刷新,只能全刷,而全刷要15s,也就是说1分钟内只有45s是正常显示的,这也太鸡肋了,完全没有实用性可言,所以最终决定就只做日历,一天刷新一次,功耗也是嘎嘎低
3 y/ r' B* M3 G9 U" y! R( u6 H; P, a
) x- t% b1 I0 C0 p
由于中途有很多调试工作,我就不再一步一步讲解是怎么做的,简单讲解一下demo功能、接线、代码逻辑、上位机使用注意、后续改进想法等/ i0 H, U8 _- s4 Y- O* s
7 x, w! p9 |/ q3 `+ ^$ O3 s) T- l* u- v
一、简述功能
- R- H, j! x5 j$ k& Q使用STM32U08 NUCLEO开发板外接一块我自己做的电子墨水屏驱动板(软硬件已开源,地址:https://github.com/BUYITAO/My_E-Paper_Driver),显示当前的日期,每天24点整由RTC闹钟唤醒刷新屏幕,平时MCU都处于STANDBY,这样可以让整个系统处于最低功耗状态。正常显示如下(偷懒了,没有做界面美化设计,请见谅 )
# W7 A/ S9 |. ~7 F* T: ?
( t, v# Z# c; Q m9 s0 X! G. O; ?. k- K2 R& {3 z4 @5 r) W* m9 L! `
系统首次上电后由于RTC参数丢失,会处于默认的预设状态,需要用户手动设置一下时间,此时电子墨水屏显示“Please config RTC data”,如下图所示 ( Y* z2 }5 Y( u9 i, t
+ V) B7 t% S {2 b5 I$ j$ w
1 b6 l; ?$ a4 c7 \7 M9 K, I) `8 R- l# b
9 I# G/ s* `* @) z
用户可以通过串口发送配置参数(这个我做了一个上位机可以一键发送配置指令),MCU收到后,刷新屏幕,再进入standby。上位机界面如下图 4 A# j) ?2 U2 N9 Y5 y; ?8 a6 s! }
f$ N$ Z$ o6 q3 t( g) _9 H
6 _6 W$ E" P, Y; [. `
- Y# b1 T5 Y4 ^ N( s
" M3 A" V, A. x4 _ O4 o5 k& \5 t; d% o6 C5 u+ ~" b
二、硬件连线
. N% v# P6 G) m U
% c1 j; k A- I/ \/ f1 [" L- d
- x5 w3 z! Y$ n$ M' Z! t
' l% g7 K: i2 f# ]+ F y% O# _墨水屏驱动板 STM32 . p% @$ _7 U4 r1 l+ {% ^
MOSI PA7 3 X6 d8 e9 G) U# q2 J
CS PC9
! P" h* T/ F6 CRST PC8 % `/ W. c! w3 v+ |4 q' a' M( x
CLK PA1 + R( F3 q% U' N; M* w
DC PC6
) ^' Y" S6 Y% ABUSY PC5
5 \$ z: Y2 e. O. j+ d5 Z+ `* J- U9 S3 L0 ?, M: ]: j
" Q& d- B y4 C1 w: { n9 \( W, T3 M
照片不太看得清楚开发板上的接线,我把原理图放上来 7 ^0 ?( p2 a% c" U& N$ e- c( a
5 c y3 R1 W* p/ w
8 F) E3 {# `3 G; H* ^7 f
& ?, f2 u* X( G% E1 ^然后串口部分我借用了STlink的VCP(这个最后改进点部分我会讲到,用这个其实对功耗影响挺大的,后续打算改掉)
5 ]; N" N2 U& b# f) G, y
& _& G h X# S5 ]. [4 I9 L3 L5 O/ Q1 g' ^
三、代码简单讲解
n; G0 [! m& @5 q: w
/ Y) [; D O$ O7 l2 uCUBEMX打开SPI,然后配置了GPIO。这样就可以实现与电子墨水屏的通讯 - u: }' _: z* D. Y. n
& e& w2 l. R9 t
- ~4 M; X0 b: Z- g7 Z3 X& s/ u 0 F, F _/ S+ w# }( [6 |; H; d
' @" c, {8 `4 Y% @/ z \0 H
" a7 }' @; ?9 {. Z- j4 d
6 ~) Y0 c# V% v
然后把GPIO和SPI的库改成LL库(个人习惯,这些基础的外设以前LL库用惯了,毕竟可以看代码学习对应寄存器的操作),然后生成代码即可
6 q+ Y5 d _8 x- I. c# `
. L1 ?- r2 B1 U5 p
/ r4 j2 E$ b. _; G: ^***这里需要注意一下生成的代码有两个BUG,毕竟现在U0的PACK才第一版,有点BUG很正常,希望ST可以看到我的这个文章,在后续的版本中修复一下 0 ]+ G8 Y' j: ~, [5 v# ^
第一个是在main.h中,红框这个没有换行,会导致编译报错
- V8 b2 K3 k8 D) J( Q) @2 ^
2 [. D1 c) S l. V. d
- u% Z6 r) F; Q' H# P) A& K
第二个是生成的工程会强制设置为V6编译器,V6编译会疯狂报错,改成V5就好 " @6 X, `6 K5 H) X0 J5 C5 t
# v) ~$ Z, s: p; q$ P+ j& ]
5 C. D' c5 ~- S4 M# L* k( O5 B9 x
. A S' o' F" E& _8 c/ U. `9 I% l/ }8 Q. j1 L7 }9 \
1 y l: f! x* f% N# h
然后关于电子墨水屏的移植、配置啥的我就不细讲了,在我分享的github上有详细的readme。接下来简单看一下功能代码 4 c$ d, F k# I7 ~% [0 q% J
while1前我会初始化墨水屏,然后判断是否为首次上电。如果是首次上电,就让墨水屏显示“Please config RTC data”,反之就清除标志位,刷新屏幕(到这里就是RTC闹钟到时间了,要刷新屏幕,显示新的日期),然后再进入Standby - /* 初始化墨水屏 */; @- ?7 r. o0 |7 {0 b8 u
- E2213JS0C1_Init(0);
; t! S# C" B3 Y3 p1 [) J - /* 是否为首次上电 */! l1 `, l3 U( d5 V! D
- if(__HAL_PWR_GET_FLAG(PWR_FLAG_SB) == RESET)" D: U# `# w0 g4 v4 F4 v) Q
- {
/ t. H! a. Z$ }5 d" g u - printf("normal run\r\n");7 L+ W4 N2 R! h3 |0 u8 ?' d' c
- /* 打印RTC时间 */, X8 G! c! }4 E/ X; H* z6 O) c
- print_rtc_data();3 ~$ }' c! r* }" `8 J+ o ~# J
- E_Paper_show_first_power_on_page();
5 }7 y! ?, g0 `% n! {+ d. n) [ - }
! K9 a$ M/ g0 ` A* W! c - /* 从standby唤醒后的复位 */" J/ G4 _$ ]: M
- else
" n, @! K, U+ n- G5 N - {
]0 `5 w- w+ [: v- u. W9 { - printf("wkup from standby run\r\n");% H( ?+ x* B, B. A- {( s
- /* 清除standby的标志位 */. V1 e" E" G: w2 ^2 y9 ]( ^0 T
- __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);6 F6 k% k ^5 s
- /* 清除闹钟标志位 */
- b, N1 R# O" e3 d" f4 L - __HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
+ a" |5 R8 m" m( u* e& _4 ]" q - /* 刷新屏幕 */
7 j3 Y6 y( j" X6 o - E_Paper_show_calendar();
5 P, N8 k$ t/ _5 m5 n" Z# U( v6 M9 T - /* 进入standby模式 */
" w/ z4 M# n( L- k; Z - HAL_PWR_EnterSTANDBYMode();
0 K, M# q7 Y1 X; W+ k8 U( z - }
复制代码
( {( L" m5 ?# ]" P8 w
; j8 T# _( P; S
8 V7 [; p; w3 |& f
如果是首次上电的,可以看到没有调用standby函数,所以他可以向下进入while1,while1中的函数如下 - if(HAL_UART_GetState(&hcom_uart[COM1]) == HAL_UART_STATE_READY)5 u- o! F: z8 A. R
- {$ B2 `! u4 a4 L M0 ?* `# }
- if(HAL_UART_Receive(&hcom_uart[COM1], (uint8_t*)&buffer[idx], 1, HAL_MAX_DELAY) == HAL_OK)
) ?# A3 K1 ~0 ? - {
6 x9 q* K6 V1 ]+ w I+ r/ y - if(buffer[idx] == '\n') // 假设\r\n'作为结束标志
# s+ E/ r, d( j5 V9 b - {, j! F! T" |2 W
- buffer[idx] = 0; // 添加字符串结束符" ^1 u' H3 G& g$ a
- idx = 0; // 重置缓冲区索引) c6 e) y1 k' {' B$ V
- if(ParseATCommandAndSetRTC(buffer))
! q0 i3 B7 W7 e& l - {' T% {! T3 D7 W J/ c( K [
- printf("Parse AT Command success\r\n");) {4 R* H; ^! G5 q
- /* 刷新屏幕 */
+ z5 \3 E, L, S6 `/ h/ M& @$ y - E_Paper_show_calendar();/ h, a5 d+ v- q/ T8 j
- printf("enter standby\r\n");: s, d _+ Q9 K* i4 w5 [6 R- |. a
- /* 进入standby模式 */
6 H/ H6 g* a* A - HAL_PWR_EnterSTANDBYMode();
7 H9 n' K- Y% e+ h: z/ [# I2 x - }
2 Z5 {# z K d2 T - else4 P% u6 M' r1 B
- { T0 E7 ]- U: E/ y! D8 c$ c/ ?6 S
- printf("Parse AT Command fail\r\n");! S N) A! C6 C/ q7 k' u
- }
! _9 p) V. T# |; w9 f - }
, C8 u; ]/ x; ~1 | - else- _5 t) h# [1 ^, x: @' C6 x1 L( p
- {, s- p- z, B- ]7 ^8 L/ V' k
- idx++; // 缓冲区索引增加, y2 W/ i% Q2 r; R& B
- if(idx >= sizeof(buffer) - 1) idx = 0; // 防止缓冲区溢出
5 l2 k. D) q$ A5 [0 n: u - }( P) | K7 y9 Q/ d7 X3 V
- } N" `% u% b/ |0 d, W8 Z3 h
- }8 \2 k A0 C+ N/ j6 `
复制代码 C x' Z7 O9 X! }! W
i0 b5 }* F7 n
: Y" s1 x4 A1 c3 r% m# Q! U
7 ?. ~/ a- s/ D$ F- h) ]4 M会一直去获取串口收到的数据,如果找到\n就认为收到一包数据,然后去解析,解析成功就刷新屏幕显示日期,然后进去standby。反之继续接收 + _8 Q: ~1 r, \% ^. s' B: m
下面看看解析函数 - bool ParseATCommandAndSetRTC(char* buffer)
, {, |* |. Q/ M9 H4 E - {3 ?: W ]( n) F: d) M& x
- RTC_TimeTypeDef sTime = {0};7 j) Y5 I8 u# [8 {& U% I. @. T+ |# X
- RTC_DateTypeDef sDate = {0};0 U2 A" o' h/ Z' E% b3 n; w
- unsigned int temp_year;; Z8 y) [3 L! s+ a1 o
- unsigned int temp_month;
& ?1 H/ e! k6 P - unsigned int temp_day;5 o# q1 x+ D0 D' c* Y- }# [2 ]
- unsigned int temp_weekDay;
7 v: d" L& G1 o( r7 k5 e J - unsigned int temp_hour;
1 @5 q. e5 Z6 R# T1 T9 W - unsigned int temp_min;* O) T. }9 Y5 e* j; Z
- unsigned int temp_sec;7 o0 a+ s3 u/ u$ D! h
- / \1 s7 v9 o& z
- /* 检查命令的头对不对 */
& W( N# j& t$ p* m; J/ x - if(strncmp(buffer, "AT+configRTCdata=", 17) != 0) return false;4 |/ Q* X) L" L1 F
- , J/ L: v' \' O
- /* 跳过命令头部分,再跳过年的前2个数字 */
8 Q1 }7 d) Q& j6 V" T5 n; s$ F7 B - char* dataPtr = buffer + 17 + 2;
% K5 b4 r. [+ f; b, { - 6 t/ S9 d: O* L9 N
- /* 解析年、月、日... */2 ]+ s% f" Z( Q6 K; O
- if(sscanf(dataPtr, "%2u-%2u-%2u-%2u-%2u:%2u:%2u",
7 r4 c5 {/ F* m1 O9 _# I - &temp_year,
x5 k$ p/ h2 u: w j - &temp_month,
6 P! g( S: J0 o, h* ~$ C! L6 t5 W - &temp_day,
) f: h9 C) W! B% {( u$ q - &temp_weekDay, 8 s- u0 m) _6 V2 C9 L
- &temp_hour,
! b; l9 U4 W0 z5 Z7 M4 z - &temp_min,
) R; m% J1 g) L& m - &temp_sec) != 7)! i. \4 }" Z% ]9 F
- {
Y; t9 m& D+ |( W' m - return false;
6 n' r6 _6 Y% t' w( V# e - }: V. L6 X- {1 S: ?" ~" w
- 2 Z/ M; P+ ?/ j* w) ?0 T
( z) n$ H! B% S1 L- sDate.Year = uint_to_bcd(temp_year);
- L# d. K" H( i6 ~, u - switch (temp_month)9 @0 k' v4 B5 m, Q: U
- {
5 F! m4 S4 e, E- S% N - case 1: sDate.Month = RTC_MONTH_JANUARY; break;5 e& H3 y y6 P; y/ `- J2 p
- case 2: sDate.Month = RTC_MONTH_FEBRUARY; break;% p! G9 E7 n- w& x0 c; e+ N
- case 3: sDate.Month = RTC_MONTH_MARCH; break;
+ f5 X Q3 z, V! a/ q - case 4: sDate.Month = RTC_MONTH_APRIL; break;
- L# k& z; t" D( t' H; I! e - case 5: sDate.Month = RTC_MONTH_MAY; break;' s' E1 z* O* ~% t7 F6 y) A2 O9 S4 V
- case 6: sDate.Month = RTC_MONTH_JUNE; break;
! _/ K5 ^% `% @ R* g6 z. \ - case 7: sDate.Month = RTC_MONTH_JULY; break;1 l; I4 \! m4 r5 y+ [7 J& u6 I9 y
- case 8: sDate.Month = RTC_MONTH_AUGUST; break;
% B. `* F* }4 ? - case 9: sDate.Month = RTC_MONTH_SEPTEMBER; break;3 M1 p. b" `; Z7 M& b) {" A/ t
- case 10: sDate.Month = RTC_MONTH_OCTOBER; break;
3 |3 g- |- D2 S8 B" J9 G# ^ - case 11: sDate.Month = RTC_MONTH_NOVEMBER; break;; n% O3 f2 Y* g% i+ a' ^# _
- case 12: sDate.Month = RTC_MONTH_DECEMBER; break;4 U* ]' j: u6 b6 s+ `# I- S( ^
- default: return false;" y+ h) o) b; a
- }7 U9 T( f' ], x5 B" R3 P3 s2 N' a0 o6 K
- sDate.Date = uint_to_bcd(temp_day);
8 c& I) h6 }3 T$ _" C1 Z - switch (temp_weekDay)
5 n3 A2 Y, P$ b+ v7 |4 O( W - {9 ?: g& S% c; G! H9 s( E
- case 1: sDate.WeekDay = RTC_WEEKDAY_MONDAY; break;* N2 ` d, S, Q8 C$ ~
- case 2: sDate.WeekDay = RTC_WEEKDAY_TUESDAY; break;' l) o3 L. }$ i3 o% I# ]' Y# `
- case 3: sDate.WeekDay = RTC_WEEKDAY_WEDNESDAY; break;
' F0 S7 g0 D9 w2 M$ n* l- l9 v - case 4: sDate.WeekDay = RTC_WEEKDAY_THURSDAY; break;
/ e4 s" B- W2 N& y! C" V2 h% a/ \ - case 5: sDate.WeekDay = RTC_WEEKDAY_FRIDAY; break;
% t6 z. u/ t d# q1 ?! v& V - case 6: sDate.WeekDay = RTC_WEEKDAY_SATURDAY; break;
1 x: C u2 S, C a - case 7: sDate.WeekDay = RTC_WEEKDAY_SUNDAY; break;
/ b. p, G4 u0 j% u9 q$ d0 r - default: return false;
/ q1 j8 ^* I7 x; S0 L9 q - }
. u: V$ R- X- V5 |7 v
7 n. q& V7 [) w" u9 n$ B* d$ V- sTime.Hours = uint_to_bcd(temp_hour);
. M; Q! h5 }- p( [& f - sTime.Minutes = uint_to_bcd(temp_min);1 d, @# `$ j& b7 X# Q0 }, p3 D
- sTime.Seconds = uint_to_bcd(temp_sec);
; |* M B! X/ k1 y - sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
( g# \9 x. s- S6 h+ o - sTime.StoreOperation = RTC_STOREOPERATION_RESET;
' N) a8 S. O5 y9 b
8 O. Q+ o, H8 @* j- set_rtc_data(sTime, sDate);
' H" R/ U3 K3 g! {8 q2 a; ~; a
( a! A) c: s7 W( d. A H- print_rtc_data();
; }) {" ?# d% B* s* R! `. c
4 [( B. X9 [5 y- return true;! ?4 K6 D, o6 Y4 f1 `- W
- }
* X2 b2 i4 T" X, _
复制代码
$ ^3 g. p8 O6 o7 y# C3 J4 J如果解析成功就设置RTC,反之返回false。
) |. U2 s) E" P. s) H2 L! X" `5 O- u" L2 ~
我输入的串口数据是十进制的,但是RTC使用的是BCD,所以需要转换一下,这就是uint_to_bcd函数,他的内容如下 - uint8_t uint_to_bcd(unsigned int num) 2 `1 m6 m2 `) I y4 f
- {0 Q% i1 v8 Y- J3 a- I% V
- // 检查输入范围,确保0-99之间, h5 I9 @* L) h1 p$ u4 R
- if (num > 99)
( ? L* _* X' Q. f7 X - {8 w6 b* X& X2 g8 e! C5 n
- // printf("Error: Input out of range. Maximum 99 allowed.\n");5 X) V8 S+ ^# i! x
- return 0;
: q. ?9 F5 W" F, L' ]4 Z - }
5 X: x# I- }) m b7 K; i9 a9 a
" J- ^. G4 f) w/ N3 E1 S( l- // 将十进制数转换为BCD: r% Z. u) e8 Y* E& y7 B1 `% _
- uint8_t bcd = (num / 10) << 4 | (num % 10);) {* F6 _" Z3 o' J6 d! T
- 3 `& i. N3 w. g2 ^2 i$ v8 v
- return bcd;
& O$ @( U k. U& G1 r, V - }
+ }3 ?7 s6 _: S) M
复制代码 * V! w# Q+ f% j. B6 |8 V1 L, D
核心的逻辑代码都在这里了,屏幕显示的函数与具体逻辑无关,我就不展示了,可以自己去看附件中代码
* ~6 a/ l: f3 T- H. {4 g O9 N8 K7 g7 F$ }# @- h: O
四、上位机注意事项 3 }' {* x2 C. }, @
上位机软件是用python+tkinter+serial写的,所以保证你的电脑上安装了python环境(我这边是最新的3.12.4),然后要安装tkinter、serial的库,否则运行时会报错:“ModuleNotFoundError: No module named ‘tkinter'”或“ModuleNotFoundError: No module named ‘serial'”(我忘记是serial还是pyserial,大致就是这么个意思)
( C( [3 r; _' f: k$ Y f# b
2 K& @9 c# z/ c2 B; F) {( ]# p
* B2 }% d: M; b6 ?' H7 X你可以用以下指令查看是否安装这两个库 如果安装了,会弹出一个窗口,这是tkinter的一个demo 如果安装了,会打印 出可以用的COM口 + N6 \7 J" B! I
如果你没安装,可以用以下命令安装serial tkinter一般都是在安装python时自动安装的,如果你没有可以看一下这个 文章 / a+ W+ v+ S: L% r9 e
然后上位机我也提供源码在附件,如果EXE实在无法运行,可以尝试用VSCODE调试运行,我这边之前就是调试可以运行,EXE无法运行,需要安装东西,很奇怪
/ M* y) A+ L2 N然后便于测试,我提供了强制写入23点59分30秒的版本,可以通过注释以下代码来实现 ) j0 K" |; w* m
( K# _" p; I/ |5 ^" ^
& K2 _( R% Q- z7 k
: a3 l- b6 d) X9 X8 w; M五、后续改进想法 - |) S/ e: E, z7 m4 l0 n2 K4 _ i
这个demo现在其实处于一个初始状态,低功耗部分还没有调试过,目前已知的硬件上肯定需要做调整,否则功耗挺高的 E. U* k( E6 v& W2 K# n
1.LED3这个灯要干掉,他一直亮着耗电 / P4 w0 S& V' i K/ _
2.供电及串口,之前我测试standby功耗是是选择使用CHG跳帽的,现在为了可以借用STLINK的VCP被迫选择STLK,这会增大功耗,后续我打算使用外部的串口,供电改回CHG。板子上需要把SB48/45焊接一下,这样串口可以引到arduino的D1、D0口上,配置完时钟后拔掉接线
1 h% k0 z6 m" \0 u4 L7 u7 d$ B
7 H( W8 z. Z0 m* c" J
: P$ E2 r+ K# W* E/ h
7 F& `" A/ P! h4 n* |3 X/ `3.STlink VCP的R23、24电阻干掉,我一直怀疑MCU会有电从这个漏过去,导致之前测量的功耗高,反正现在打算用外挂的串口,这个我就直接干掉。 0 \9 V% g$ M9 o: S. q
4.配置RTC的机制有点不灵活,用户想要配置RTC,必须断电再上电,不友好。毕竟RTC时间长了就会偏,手动重校时是必须的。后续可以把板载的用户按键利用起来,把它作为唤醒源,当按下后,MCU WKUP,等待用于输入配置指令。多久没收到正确的指令就再进入standby 5 g- o8 x: [; r) M: k! I* y
5.美化一下界面 & W( {' \1 l' ~3 J- m, {' @
6.增加wifi模块,这样可以实现每天网络校时,并且还可以获取天气预报信息,让屏幕看上去不那么空 5 I/ k! y8 h7 P5 D: \! d9 ~
7.如果有合适的液晶段码屏,可以加上,用于显示温湿度数据,躲开了电子墨水屏的问题 $ m# N- b: D/ Q' D4 j
, S6 b4 z, ^8 m/ V2 w) K, K5 J5 X0 a: w3 v% ]* T9 J5 g
. j2 R+ u2 C3 z) C; c" C8 S# M% O$ h
感谢各位读到这里,如果你有更好的想法或者有疑问或者代码有错误,欢迎在评论区交流
- f7 u! M0 L) M! \, F4 [# e; _ |3 W- r: o' t1 [# z0 w8 x
& p. S8 N/ H0 f! H6 t7 _ N
% m$ t) z9 R* @) r& C0 ]
! n' L7 j7 n' G- V2 |+ D @' i项目源码及上位机如下
3 f# [. s8 z9 s1 o' Q' M" b5 C |