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