本文是STM32U0测评系列的最后一篇。我认为STM32U0最吸引我的就是他极致的低功耗,ST官方也说他非常很适用于水表等产品的应用,于是我只做了一个电子墨水屏RTC日历。 W. }" n+ g2 P* q- j) i% W/ |
7 ~6 E; D& Y( J9 y, ?' p/ O5 ]4 u! o7 Q3 E$ K1 c6 {) p
一开始我是想做日历+时钟+温湿度。但是很可惜我手上的这块电子墨水屏不能局部刷新,只能全刷,而全刷要15s,也就是说1分钟内只有45s是正常显示的,这也太鸡肋了,完全没有实用性可言,所以最终决定就只做日历,一天刷新一次,功耗也是嘎嘎低; b, k$ R0 N* A+ M
4 i# Z9 E P" ]' l$ Z) r
) o. T" U2 p9 x+ _& Z7 X1 f- o由于中途有很多调试工作,我就不再一步一步讲解是怎么做的,简单讲解一下demo功能、接线、代码逻辑、上位机使用注意、后续改进想法等/ q0 R& r# }3 E# u7 G$ }3 i
# M7 F9 u; q4 u f1 [5 s2 N
0 c; n6 t) t$ p5 i6 c9 R, J一、简述功能; T8 D" v) Q X
使用STM32U08 NUCLEO开发板外接一块我自己做的电子墨水屏驱动板(软硬件已开源,地址:https://github.com/BUYITAO/My_E-Paper_Driver),显示当前的日期,每天24点整由RTC闹钟唤醒刷新屏幕,平时MCU都处于STANDBY,这样可以让整个系统处于最低功耗状态。正常显示如下(偷懒了,没有做界面美化设计,请见谅)
9 C! I7 ]2 E+ T0 u2 O$ l2 H/ ]
% O6 B3 ?- U; u" F+ @& k& D# S* ]- e+ F1 M. y: n+ f# m6 F/ E) N% D% f% a7 b
系统首次上电后由于RTC参数丢失,会处于默认的预设状态,需要用户手动设置一下时间,此时电子墨水屏显示“Please config RTC data”,如下图所示 + J' n" u7 ]0 y1 J& |
" \8 | w K0 y7 G5 |
; H; w% w+ O* C
$ D9 z' p6 `5 U, ~用户可以通过串口发送配置参数(这个我做了一个上位机可以一键发送配置指令),MCU收到后,刷新屏幕,再进入standby。上位机界面如下图
8 @0 z9 _9 \7 |& Q, D% O3 w5 Q# H
6 ~( v, f/ C C5 g" Y' H
) E7 Z' e/ D8 Y4 o% x: h8 a5 j( i$ d6 Q+ p4 r$ O1 P
# j3 k7 a; e0 O$ A
" B1 N& x' o. u2 ^( Y3 u x二、硬件连线
& j9 S% ?( F! w: x% P
; I7 Y" x! I& q3 H) [6 n
; H; I4 R4 r `8 w8 X* c
" b: L V7 |5 \# {8 X5 k/ b9 f8 t6 v墨水屏驱动板 STM32
' E9 _$ T& v1 z' t4 k. K3 uMOSI PA7
- a) b9 H- W, N BCS PC9 1 X0 `& ]: b4 |, V
RST PC8 " @6 H* D5 g& N! z( W9 H
CLK PA1
W& e L3 d# J, ~0 dDC PC6 * S, x4 i4 g; U, b
BUSY PC5
4 Q+ ?; S& A/ m* z9 p: t' u( _& F* @! u
: l: H. d8 E; s# l# m6 z/ @
照片不太看得清楚开发板上的接线,我把原理图放上来 . B) c- c* x, B$ F( H
/ d3 U3 I: O j$ }$ L$ i4 _- U
6 K& A5 n' l8 g" I" t- \. D f, C! R
然后串口部分我借用了STlink的VCP(这个最后改进点部分我会讲到,用这个其实对功耗影响挺大的,后续打算改掉) 1 m+ \* }" n6 }8 l* a
; z" l7 w' V& c d9 {1 U/ H
( U4 v% h1 u" \6 e# E3 g7 x三、代码简单讲解
0 O( u1 T6 o5 ~' r' U; Q+ G1 L8 W/ F6 w4 j6 R: W1 k
CUBEMX打开SPI,然后配置了GPIO。这样就可以实现与电子墨水屏的通讯 ! N/ k% Q1 ~6 J
& k/ R- `: Y- P0 o! Y+ U' U8 A
" m+ M4 z. H* D; Z; j3 n0 P
A$ y' { e$ O0 e1 ^2 g
% K8 N! Z( R6 J# O
}2 [9 \; f: y! l, J
7 y" q7 }, C7 D4 e9 H7 _- V$ V/ z
然后把GPIO和SPI的库改成LL库(个人习惯,这些基础的外设以前LL库用惯了,毕竟可以看代码学习对应寄存器的操作),然后生成代码即可
( W$ {- O9 ]/ |. n, x
( @% { p1 A8 v" s8 {
# i2 F3 ^8 c' I) a( u5 O9 v+ F1 ?***这里需要注意一下生成的代码有两个BUG,毕竟现在U0的PACK才第一版,有点BUG很正常,希望ST可以看到我的这个文章,在后续的版本中修复一下
7 g7 ]4 z% `& y; B4 k# |( t8 J1 x第一个是在main.h中,红框这个没有换行,会导致编译报错
6 Z. q/ B, j. P
" w% q3 B' T& \
3 R3 q e6 A- d5 q$ q2 ^( T4 t第二个是生成的工程会强制设置为V6编译器,V6编译会疯狂报错,改成V5就好 + C/ E) j \* p( Q l+ I
* p" G/ p8 o0 X* F) O1 v- `
6 H- U6 d- K" [$ Y$ V; o3 _/ q
. h/ m+ M. |- s+ X: ]4 T9 b, I! o$ b2 M) R4 |5 r# I
% L& F0 ^+ [9 P4 ]& U- C/ I8 N, g
然后关于电子墨水屏的移植、配置啥的我就不细讲了,在我分享的github上有详细的readme。接下来简单看一下功能代码 ( n3 d& T7 p& Q' F; A
while1前我会初始化墨水屏,然后判断是否为首次上电。如果是首次上电,就让墨水屏显示“Please config RTC data”,反之就清除标志位,刷新屏幕(到这里就是RTC闹钟到时间了,要刷新屏幕,显示新的日期),然后再进入Standby - /* 初始化墨水屏 */4 @8 d9 X, Z) u: v0 p
- E2213JS0C1_Init(0);
, v6 a. n( k X' }/ r/ ?; D& @; j - /* 是否为首次上电 */8 s0 S+ y$ c G0 }
- if(__HAL_PWR_GET_FLAG(PWR_FLAG_SB) == RESET)$ N& ?0 J1 N( j, J' |! @: h8 ^
- {
9 A8 e G% V5 B' ? - printf("normal run\r\n");
8 q/ g' f6 q1 Y2 _, K+ `- C - /* 打印RTC时间 */) _+ ]2 X ~! Z9 P% G
- print_rtc_data();
$ W2 G1 e$ l% ~0 t - E_Paper_show_first_power_on_page();% L) \. |* W6 `# l: X
- }4 |. k9 i' d9 A
- /* 从standby唤醒后的复位 */
7 ?) S$ S. J+ A6 C' G1 X- g( _ - else
b/ V d r1 ~* A8 ~3 |$ e2 }1 Y - {: W# d- g9 s; E |
- printf("wkup from standby run\r\n");- u5 l2 u3 ~. F" R" l9 d
- /* 清除standby的标志位 */ S; x. K1 W2 _" u3 O
- __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);
9 }( z2 _1 Z5 G. E& n4 u0 l! ] - /* 清除闹钟标志位 */# j; ?- i! Z5 h3 w
- __HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);" p ^* H$ d" i
- /* 刷新屏幕 */* v5 U) X: [# F8 \( k
- E_Paper_show_calendar();
! O+ I, n3 k- ^ - /* 进入standby模式 */1 R1 }! J$ y3 L
- HAL_PWR_EnterSTANDBYMode();8 l3 f5 y5 t' u8 w. I6 h2 R/ }
- }
复制代码
- b9 H. e% z8 Q- h7 }* X; P1 U2 W
/ n4 A+ N L |
" z) k5 V, ]0 I9 I4 J3 a
如果是首次上电的,可以看到没有调用standby函数,所以他可以向下进入while1,while1中的函数如下 - if(HAL_UART_GetState(&hcom_uart[COM1]) == HAL_UART_STATE_READY): R7 Y6 d: e; V% T# m
- {
2 [. M7 ]9 v6 H" T2 {) R - if(HAL_UART_Receive(&hcom_uart[COM1], (uint8_t*)&buffer[idx], 1, HAL_MAX_DELAY) == HAL_OK)
6 @8 y, B/ ?( L! W - {
% ^9 e4 U1 f$ y2 \: a' |5 d7 i - if(buffer[idx] == '\n') // 假设\r\n'作为结束标志" D z- S6 Q4 O) I' q
- {
. H9 a* z0 {2 I s - buffer[idx] = 0; // 添加字符串结束符: R* P# t3 K& z6 D% t
- idx = 0; // 重置缓冲区索引
# t3 i/ O6 e4 g1 r+ s0 i - if(ParseATCommandAndSetRTC(buffer))
4 F2 F. ?: Z3 H# H& n9 Q - {
9 u: w8 k8 V/ f. o! u+ R" g - printf("Parse AT Command success\r\n");& T m! q2 r5 w+ e
- /* 刷新屏幕 */
- t9 h9 S! {% X3 I - E_Paper_show_calendar();. D/ m) v' A9 t9 _; q
- printf("enter standby\r\n");! L* X3 |4 d/ F4 w
- /* 进入standby模式 */2 z- N @9 I- Q4 r K9 i G9 i
- HAL_PWR_EnterSTANDBYMode();
1 B4 g" j* s" f# W4 y/ d% @ - }
3 b$ T8 Y F' p' L - else
* n1 ?6 h. s* J! F- s R - {' V o# y# d. _( U% `3 e3 j
- printf("Parse AT Command fail\r\n");
; _1 b6 ?7 k+ A. {# n% | - }
. m C) e! ?* s i+ K2 ? - }
8 M. S$ g5 |3 R% A - else
) w7 z! N$ L! v - {
' w" k3 w* j. A* m, _: g8 n8 N - idx++; // 缓冲区索引增加
2 O3 |0 x5 p+ R4 E - if(idx >= sizeof(buffer) - 1) idx = 0; // 防止缓冲区溢出- B. Y2 h3 {' v( l% T
- }1 F0 h6 R1 C1 L8 h1 `
- }/ H8 |7 x9 ^) \8 q0 Z: {& C3 ]3 T5 O- A
- }2 _2 E) a8 ^* N$ {. P
复制代码
V! ~ k" D. _6 v# G3 M3 h- i: k6 N7 [- o4 ~
- F/ X/ G' r8 W. z# _. U$ L- m4 S" S8 d0 y; c6 o/ y
会一直去获取串口收到的数据,如果找到\n就认为收到一包数据,然后去解析,解析成功就刷新屏幕显示日期,然后进去standby。反之继续接收 $ S7 C6 |1 x$ E% ^/ s+ i( V1 W
下面看看解析函数 - bool ParseATCommandAndSetRTC(char* buffer)* f/ Q- Z! C8 m. H
- {$ A, A( h$ f/ Q) Q. F8 ^) G a
- RTC_TimeTypeDef sTime = {0};
) G" a% [- {% r# j I - RTC_DateTypeDef sDate = {0};
z- T9 [2 ?+ G: a - unsigned int temp_year; [, N, q8 ^5 t$ d" I* l
- unsigned int temp_month;* w$ {( h) a1 m) V$ S
- unsigned int temp_day;
) Z: U* X, h2 V. G( E - unsigned int temp_weekDay; T) ]" d( o' Z+ ?
- unsigned int temp_hour;- J- C- u% N7 h
- unsigned int temp_min;1 S9 A) l4 V5 F" K) @6 j
- unsigned int temp_sec;; i# v& ^( Z3 |' ]7 f( U% h0 p
: I4 L' L% _9 ?4 _ i+ |, x4 R( x1 |, u- /* 检查命令的头对不对 */6 S3 \8 | @( g4 D
- if(strncmp(buffer, "AT+configRTCdata=", 17) != 0) return false;
' }+ o: o! N1 t! F2 e+ a, F9 @ - - [0 ~1 _! [7 W/ C4 ] X" F0 m
- /* 跳过命令头部分,再跳过年的前2个数字 */
( f$ Z5 \6 @, N" W q. b& L ]9 b+ a - char* dataPtr = buffer + 17 + 2;1 m* C! \$ G5 u7 N# c( I( ?
: @& X/ s* k% ~# Y* D# z) x5 W8 [, g- /* 解析年、月、日... */
5 r2 h5 D/ _: o w" r - if(sscanf(dataPtr, "%2u-%2u-%2u-%2u-%2u:%2u:%2u", d2 l2 g# `1 B1 D
- &temp_year,
- C/ D8 c8 z2 K3 Z$ R% L - &temp_month, 3 v+ Z5 D7 D( m9 B) R5 K
- &temp_day, / i$ }8 I( A5 V6 {# E, J7 v. S2 g
- &temp_weekDay, . T; p3 }0 S" Z
- &temp_hour, ( Z& v1 D& Z5 @" ^) O/ K. E
- &temp_min,
r! Y! ]0 t' a6 j( |$ ~1 Y4 b - &temp_sec) != 7)
2 b; a. l% e+ n" h; p) d - {
; R, I3 C) {# T8 T8 w - return false;0 i s" }0 P: _3 g
- }' f( F9 N: e. S) d" Y
- 4 U" ?( I6 k B0 d3 n
- ; H8 U4 s. H. s- a. R9 g/ z
- sDate.Year = uint_to_bcd(temp_year);' [. |) F; P9 U; A0 ]2 x5 b1 U
- switch (temp_month)
1 k' o3 b* i- y/ N - {
3 a+ I0 j/ W9 m3 }1 ]* l; a( ^ - case 1: sDate.Month = RTC_MONTH_JANUARY; break;
& ~; f/ }: b; A8 ~, d9 W! D% w - case 2: sDate.Month = RTC_MONTH_FEBRUARY; break;1 `& t8 E o$ r* U
- case 3: sDate.Month = RTC_MONTH_MARCH; break;
% }* s& Z- z7 J - case 4: sDate.Month = RTC_MONTH_APRIL; break;
5 S) L/ K/ u. I/ K - case 5: sDate.Month = RTC_MONTH_MAY; break;
# ~9 N; o7 o3 e/ L5 R( X0 K" i! _ - case 6: sDate.Month = RTC_MONTH_JUNE; break;
6 O' f4 {( H5 k z - case 7: sDate.Month = RTC_MONTH_JULY; break;* K ~; c5 w3 F& b
- case 8: sDate.Month = RTC_MONTH_AUGUST; break;+ l* E5 m* T. e
- case 9: sDate.Month = RTC_MONTH_SEPTEMBER; break;
2 {! ~/ d j( e- H - case 10: sDate.Month = RTC_MONTH_OCTOBER; break;
& r6 B3 {- H4 }0 m& Y - case 11: sDate.Month = RTC_MONTH_NOVEMBER; break;
8 D* C! B0 E% e/ U% o2 v/ Q C4 ] - case 12: sDate.Month = RTC_MONTH_DECEMBER; break;+ O9 O2 Z' ^8 [+ ]
- default: return false;# R+ F: W- L. _: O; M& m; |
- }
! C: Z0 B+ F* \ j* K - sDate.Date = uint_to_bcd(temp_day);
( A6 v3 c& Z( y$ d" q2 P* m - switch (temp_weekDay)0 }* \' E* O8 s: I
- {" n4 K- S7 }* G- }
- case 1: sDate.WeekDay = RTC_WEEKDAY_MONDAY; break;7 G8 t; R( Q9 @ N7 S3 r
- case 2: sDate.WeekDay = RTC_WEEKDAY_TUESDAY; break;
; b. f1 \9 v/ T& X9 c' x, C - case 3: sDate.WeekDay = RTC_WEEKDAY_WEDNESDAY; break;
1 p+ r# M1 f `7 y3 q - case 4: sDate.WeekDay = RTC_WEEKDAY_THURSDAY; break;
/ \; c, l1 p1 C) I4 p/ s - case 5: sDate.WeekDay = RTC_WEEKDAY_FRIDAY; break;! l$ d) M" N' {, }9 H; }
- case 6: sDate.WeekDay = RTC_WEEKDAY_SATURDAY; break;$ |( E; m/ }- a! G# ^
- case 7: sDate.WeekDay = RTC_WEEKDAY_SUNDAY; break;
8 Z( x. b# s$ T" T7 P - default: return false;
- V c5 C* R; u( x- a. g/ q - }
% i6 M5 b J# P0 y5 z" @
- m+ I3 J4 p: q' y7 S* x- sTime.Hours = uint_to_bcd(temp_hour);+ v2 n' L# [ R+ Y& d; f! s
- sTime.Minutes = uint_to_bcd(temp_min);
" u4 P d8 s( l" V - sTime.Seconds = uint_to_bcd(temp_sec);
d4 S0 V: D+ k `4 `5 t - sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
; ^3 _, e2 S) P2 R# ~ - sTime.StoreOperation = RTC_STOREOPERATION_RESET; s; u4 M/ A, O( w6 }+ D
* D) `: i4 l+ n E) N- set_rtc_data(sTime, sDate);$ n& I4 `) X! @* P3 r" V% I
% G- d# E4 g! @- H" P- print_rtc_data();
$ \+ A! y" ^* x) T X* @ t: q7 t$ d - - T! J, r! l4 r L+ h% ~
- return true;1 K5 I) u4 M8 X6 V8 s R# ?
- } P! \5 d2 I( E+ | G
复制代码 ' E- f; B. B8 H& l1 I
如果解析成功就设置RTC,反之返回false。 ( c, n; c/ g; t C2 O6 S
8 L& c: k6 }3 q( w& j4 a& G
我输入的串口数据是十进制的,但是RTC使用的是BCD,所以需要转换一下,这就是uint_to_bcd函数,他的内容如下 - uint8_t uint_to_bcd(unsigned int num) $ o( v. j& [/ c6 _/ D: s1 L2 [
- {3 [* H E* c9 A9 ?9 L) t
- // 检查输入范围,确保0-99之间4 F( ^5 p0 s; `) j4 ^$ K
- if (num > 99)
% D" A; P, w1 N - {
% E% n @' D9 i. W - // printf("Error: Input out of range. Maximum 99 allowed.\n");
+ ~: v" n$ ?6 G - return 0;
+ ]: o# p5 _* a3 V" r5 T - }& \. m8 g* k1 m; k9 K
- n0 q, j# a$ v: c- // 将十进制数转换为BCD& O8 f% @( i+ a) F
- uint8_t bcd = (num / 10) << 4 | (num % 10);
& B B8 w( O5 f. B - 6 z9 {. E) A7 R" ^% J% a2 F; c
- return bcd;
5 j9 Q4 |/ v# \1 U - }/ k* s, c! [1 C! i* @0 |
复制代码
/ C/ [3 I0 A" L核心的逻辑代码都在这里了,屏幕显示的函数与具体逻辑无关,我就不展示了,可以自己去看附件中代码
2 ~0 T3 N: N7 c0 W6 \0 F- o, w$ s, }/ S% v4 _
四、上位机注意事项 ( s7 F- `- y+ D ~. i6 Y2 l1 T9 B/ K
上位机软件是用python+tkinter+serial写的,所以保证你的电脑上安装了python环境(我这边是最新的3.12.4),然后要安装tkinter、serial的库,否则运行时会报错:“ModuleNotFoundError: No module named ‘tkinter'”或“ModuleNotFoundError: No module named ‘serial'”(我忘记是serial还是pyserial,大致就是这么个意思) 9 }1 ~/ y/ r7 h! ]
2 s P! m. Z) R- Q+ l" B
: {. \. K% Y8 x+ i4 s( I/ o7 c" [6 g你可以用以下指令查看是否安装这两个库 如果安装了,会弹出一个窗口,这是tkinter的一个demo 如果安装了,会打印 出可以用的COM口 % O" l. @5 v% w- \: J, l- f8 r/ p3 `
如果你没安装,可以用以下命令安装serial tkinter一般都是在安装python时自动安装的,如果你没有可以看一下这个 文章
$ b4 ?3 I+ L& \& D" x5 W C( o然后上位机我也提供源码在附件,如果EXE实在无法运行,可以尝试用VSCODE调试运行,我这边之前就是调试可以运行,EXE无法运行,需要安装东西,很奇怪 5 P/ o1 f* S \* ?
然后便于测试,我提供了强制写入23点59分30秒的版本,可以通过注释以下代码来实现
4 V d. r5 v' C5 c
) i8 x9 C2 l6 ^# V K/ w( Y+ `* {; G
% H: r" e' B! J' C n/ j7 C! ~' a- X) ^
五、后续改进想法 * [& V6 h! B; m- H
这个demo现在其实处于一个初始状态,低功耗部分还没有调试过,目前已知的硬件上肯定需要做调整,否则功耗挺高的
/ P) @) ?' U& P5 B" ^; q1.LED3这个灯要干掉,他一直亮着耗电 ) f' M3 h; X. ]: v' K! a) @& a
2.供电及串口,之前我测试standby功耗是是选择使用CHG跳帽的,现在为了可以借用STLINK的VCP被迫选择STLK,这会增大功耗,后续我打算使用外部的串口,供电改回CHG。板子上需要把SB48/45焊接一下,这样串口可以引到arduino的D1、D0口上,配置完时钟后拔掉接线
h$ }. C9 V' L4 r( l
# _ {; z3 u- x7 L
) ?0 `/ u5 F2 h' V" o" h
' e1 u4 P+ Z5 Q2 l0 H7 l, E
3.STlink VCP的R23、24电阻干掉,我一直怀疑MCU会有电从这个漏过去,导致之前测量的功耗高,反正现在打算用外挂的串口,这个我就直接干掉。 , W2 s& e5 k; [% P1 j$ L/ x: C
4.配置RTC的机制有点不灵活,用户想要配置RTC,必须断电再上电,不友好。毕竟RTC时间长了就会偏,手动重校时是必须的。后续可以把板载的用户按键利用起来,把它作为唤醒源,当按下后,MCU WKUP,等待用于输入配置指令。多久没收到正确的指令就再进入standby
' p. w1 G! |* h& S! r5.美化一下界面 * _0 U. L. e+ ^6 ]& @# s
6.增加wifi模块,这样可以实现每天网络校时,并且还可以获取天气预报信息,让屏幕看上去不那么空
4 v f- s2 D8 a( s# I ]7.如果有合适的液晶段码屏,可以加上,用于显示温湿度数据,躲开了电子墨水屏的问题 + H8 J- N3 q4 `- q8 I$ p
8 R4 Y4 b& b5 ^$ }5 ]% q4 k
( W9 \+ ~! L0 d8 c) q
4 k2 g6 x3 r: {; `1 A4 G5 t5 p$ P7 ^' ~ F$ T" q1 Z
感谢各位读到这里,如果你有更好的想法或者有疑问或者代码有错误,欢迎在评论区交流 ! T- W3 n9 a) R9 S; \, M
4 V0 {, e- g0 M1 O- w) B4 E4 I, \. P% T! C, l4 h
; X6 U9 M+ r) a) |3 K8 [
$ n$ B7 H5 M' {; s; J7 Y项目源码及上位机如下
! R6 e3 O5 Q% Q9 X0 q# b) v, b- I- h |