-
Notifications
You must be signed in to change notification settings - Fork 13
/
My-19-years-old.tex
executable file
·1773 lines (1239 loc) · 74.3 KB
/
My-19-years-old.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[cn,11pt]{elegantbook}
\title{我的19岁}
\subtitle{电子设计大赛}
\author{余阳(CNYuYang)}
\institute{武汉理工大学电子科技协会}
\date{\today}
\version{1.2.0}
\extrainfo{做一个优秀的普通人,在热爱中燃烧。 --- 余阳}
\logo{logo.png}
\cover{cover.jpg}
\begin{document}
\maketitle
\tableofcontents
\mainmatter
\hypersetup{pageanchor=true}
\chapter{我的电赛经历}
我的19岁,很累。这一年在程序员的圈子里流行着996ICU的说法,但是这一年的我却过着8107的生活,每天早上8点到实验室,晚上10点离开,一周工作7天。并没有人强迫我这么做,但是我相信“强迫自己优秀,然后骄傲的生活”。在此说明,此文的写作时间始于2019年7月30日,此文并不是一个胜利者的经验分享会,而是为了纪念自己两年的付出及这两年的嵌入式开发。同时我想如果仅仅写自己的经历,那么一定没有人愿意阅读,为了使这篇文章的受众面更广一些,而第二部分是有关STM32单片机的分享,在第三部分则是我在这一年电赛经历的记录,最后的内容是我对大学不可回避的一些议题的讨论。总的来说这篇文章的前半部分是纪念我的奋斗岁月,而中间部分则是希望经验的传承,后半部分则是胡说八道。
我的相关联系方式:
\begin{itemize}
\item 个人博客:\href{https://yuyang.run/}{https://yuyang.run/}
\item Github 网址:\href{https://github.com/cnyuyang/}{https://github.com/cnyuyang/}
\item 邮件:\email{[email protected]}
\end{itemize}
\section{热血、迷茫}
提到我的大一生活,我想到的就是热血与迷茫。之所以迷茫是因为对未来的一无所知,之所以热血是因为想要摆脱迷茫,剥开云雾见青天。
导致这种状态的原因有两类:
\begin{enumerate}
\item 学校方面。开学过后学院组织了许多的经验分享会,所有的经验分享会大都是围绕着一个主题-"保研、考研"。但是刚入学的我,对老师、学长提供保研相关的数据并不太理解。那么我就迷茫了,保研的条件是什么?考研又是怎么一回事呢?……为此也立下了很多热血的flag,我要努力保研!我要保去华科!……
\begin{lstlisting}
“每个人的大学生活开始都是迷茫的,但是要尽快走出这段迷茫”。 -- 某位学长
\end{lstlisting}
\item 慕课方面。在课余生活经常看一些慕课,不可避免的接触到一些培训机构,他们大多宣传是的一些“读书无用论”,鼓吹“技术至上”。我因此迷茫,考研还是就业?学习什么技术?现在看来这对我既有利更有害,“利”在我提前接触到了一些企业更想要我们学习的技术(比如Java),“弊”在使自己忽略了基础学科的学习。
\begin{lstlisting}
“企业看中的不是你的专业,而是你的专长”。 -- 某培训机构
\end{lstlisting}
\begin{remark}
以上两句引言,如有错误,欢迎指正,如帮助到你,不胜荣幸!
\end{remark}
\end{enumerate}
我想这一年我的电赛准备之路,也是热血与迷茫的。先说说热血,在大一上的学期末,自己并没有着急回家,选择留下来参加某校外机构的培训班,开始接触STC51单片机,大一下开学后每周六也去上相关课程。并在学期末有幸通过了学院实验室的招新考试,同时参加了实验室的暑假培训,培训的第一阶段从放假后到7月底,培训的第二阶段则是8月中旬开始的STM32相关的培训,虽然在此期间并没有掌握好STM32单片机,但是也为其学习开了个头。而迷茫在于,自己知道自己在准备电赛,但是电赛题目类型、题目的种类则是一无所知的。
\begin{figure}[htbp]
\centering
\includegraphics[width=\textwidth]{me1.jpg}
\caption{ 长江边跨年 }
\end{figure}
\section{自负、努力}
对大二上学期我的评价是一个贬义词,一个褒义词。正如之前所说的我的学习受到了培训机构的影响,在我之学到了一些皮毛时,我却沾沾自喜,说了一些不该说的话,做了一些不该做的事,现在看来当时的我是自负且幼稚。可以说是一叶障目,不见泰山。然而我还是想用努力来评价这个学期。也就是这个学期我有了8107的作息时间,风雨无阻,把实验室当家。
这个学期的电赛经历,我认为自己只配的上自负。其一:学习的方向偏向于应用方面,而忽略了STM32的学习。其二:对待校电子电工实验室的任务不重视,从而被劝退。所以,给大家一句忠告:一定要重视基础学科的学习!包括高数、模电、复变等。
\section{方向、两难}
对于大二下学期我认为摆在我面前的就是方向选择的问题,以及两难的处境。争取保研、准备考研、参加工作?准备电赛还是另寻它路?我想在最后一部分,就这些议题做谈谈我的看法。
在受到蓝桥杯失利以及涂劲豪与陈同凯的鼓励这双重影响下我也再次选择准备电赛。
为了参加电赛的第一步就是通过校赛初赛.为此我选择了最有效的途径,就是用什么学什么。在校赛的电源类的题目,STM32用到的其实就是ADC、PWM与PID控制算法。我一边参考学长留下来的代码,一边跟着B站学习,以不错的效果通过了初赛,通过初赛后我们在校实验室也就重新获得了座位。
为了参加电赛的第二步就是通过校赛的复赛与决赛。复赛是三天两夜的持续性工作,因为初赛至复赛的周期很短,在此期间我也没办法再多学习什么内容,所以复赛真的做的很糟糕。而决赛只有一个上午,题目相对简单( 信号、电源结合的题目 ) ,且以硬件为主,做的效果不能说差。
为了参加电赛的最后一步就是术业有专攻的学习,仅从电源类的角度来说,编程最重要的就是3点:ADC、SPWM、PID,逐一击破即可。(第三部分有更细致的分享)
在准备比赛的过程中,我想过要放弃。因为自己半路参加电赛,要准备的东西太多了,但是“我不去想自己是否能取得成功,既然选择远方,便只顾风雨兼程”。
\section{致谢}
文章写到这,离比赛还有一周。对比赛的结果,自己已经不太在乎了。如果自己未能取得好的名次,还希望大家不要以成败论英雄,不要因此而认为这篇文章就是垃圾。自己能走到这一步,要感谢的人有很多。包括我的队友、指导老师、家人、电子科技协会、室友……
\chapter{STM32的学习}
在你阅读这个部分的时候,我希望你对STM32的寄存器及库函数的开发方式有所了解。而这一部分的主要内容则是基于HAL库 和 Cube MX的开发方式。本来我有计划写一个部分介绍STC51单片机,但是这两者重复部分太多,所以最终仅保留STM32部分,并对其做更细致的探讨。
如果你学过STC51,你一定知道STC51操作是极其方便的。如果你学过STM32的库函数,你一定知道STM32操作是极其繁琐的。传统的库函数开发方式,将太多时间花费在各种东西的初始化上。同时,如果你学过STM32F1、STM32F3、STM32F4的话,你会发现对于不同型号的STM32在使用库函数的开发方式下,他的初始化流程也是不一样的,这也是传统开发方式的一种弊端。\textbf{而Cube MX + HAL库开发的方式,则是省去了初始化的部分,让开发人员将更多的精力放在业务的处理!}但是寄存器及库函数的开发方式也是有必要学习的,因为Cube MX也可能存在Bug,如果你对寄存器及库函数不了解那你会很被动。
如有错误,欢迎指正,如有帮助,不胜荣幸!
\section{GPIO}
GPIO(英语:General-purpose input/output),通用型之输入输出的简称,其接脚可以供使用者由程控自由使用,PIN脚依现实考量可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO)
\subsection{GPIO 8种工作模式}
GPIO\_Mode\_AIN 模拟输入
GPIO\_Mode\_IN\_FLOATING 浮空输入
GPIO\_Mode\_IPD 下拉输入
GPIO\_Mode\_IPU 上拉输入
GPIO\_Mode\_Out\_OD 开漏输出
GPIO\_Mode\_Out\_PP 推挽输出
GPIO\_Mode\_AF\_OD 复用开漏输出
GPIO\_Mode\_AF\_PP 复用推挽输出
\subsection{应用总结}
1、上拉输入、下拉输入可以用来检测外部信号;例如,按键等;
2、浮空输入模式,由于输入阻抗较大,一般把这种模式用于标准通信协议的I2C、USART的接收端;
3、普通推挽输出模式一般应用在输出电平为0和3.3V的场合。而普通开漏输出模式一般应用在电平不匹配的场合,如需要输出5V的高电平,就需要在外部一个上拉电阻,电源为5V,把GPIO设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出5V电平。
4、对于相应的复用模式(复用输出来源片上外设),则是根据GPIO的复用功能来选择,如GPIO的引脚用作串口的输出(USART/SPI/CAN),则使用复用推挽输出模式。如果用在I2C、SMBUS这些需要线与功能的复用场合,就使用复用开漏模式。
5、在使用任何一种开漏模式时,都需要接上拉电阻。
\subsection{Cube MX相关配置}
\subsubsection{选择引脚类型}
GPIO\_Input-输入引脚 GPIO\_Output-输出引脚
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{gpio_1.png}
\caption{选择引脚类型 \label{fig:scatter}}
\end{figure}
\subsubsection{配置引脚}
对于输入引脚,可以配置的就是GPIO Pull-up/Pull-down。这分别对应的就是Pull-up( 输入上拉 )与Pull-down ( 输入下拉)。
Pull-up:输入上拉就是把电位拉高,比如拉到Vcc。上拉就是将不确定的信号通过一个电阻嵌位在高电平。电阻同时起到限流的作用。弱强只是上拉电阻的阻值不同,没有什么严格区分。
Pull-down:输入下拉就是把电压拉低,拉到GND。与上拉原理相似。
简单的说,如果你希望你的引脚平时处于高电平用于检测低电平,你就使用Pull-up。如果你希望你的引脚平时处于低电平用于检测高电平,你就使用Pull-down。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{gpio_2.png}
\caption{配置输入引脚 \label{fig:scatter}}
\end{figure}
\newpage
对于输出引脚,比输入多了更多的配置:
GPIO output level -> 初始化输出电平
GPIO mode -> 输出方式 -> 开漏或推挽输出
GPIO Pull-up/Pull-down -> 上拉或下拉输出
Maximum output speed 选中GPIO管脚的速率
\emph{选中GPIO管脚的速率}
I/O口的输出模式下,有3种输出速度可选(Low - 2MHz、Medium - 10MHz、High - 50MHz),这个速度是指I/O口驱动电路的响应速度而不是输出信号的速度,输出信号的速度与程序有关(芯片内部在I/O口 的输出部分安排了多个响应速度不同的输出驱动电路,用户可以根据自己的需要选择合适的驱动电路)。通过选择速度来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。高频的驱动电路,噪声也高,当不需要高的输出频率时,请选用低频驱动电路,这样非常有利于提高系统的EMI性能。当然如果要输出较高频率的信号,但却选用了较低频率的驱动模块,很可能会得到失真的输出信号。
举个栗子:
1、USART串口,若最大波特率只需115.2k,那用2M的速度就够了,既省电也噪声小。
2、I2C接口,若使用400k波特率,若想把余量留大些,可以选用10M的GPIO引脚速度。
3、SPI接口,若使用18M或9M波特率,需要选用50M的GPIO的引脚速度。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{gpio_3.png}
\caption{配置输出引脚 \label{fig:scatter}}
\end{figure}
\newpage
\subsection{编写业务代码}
\subsubsection{初始化及重置相关}
\lstset{ language=C}
\begin{lstlisting}
//初始化引脚
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
//重置引脚
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);
\end{lstlisting}
\subsubsection{IO口操作相关}
\lstset{language=C}
\begin{lstlisting}
//读取电平状态
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
//设置引脚状态
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
//转换引脚状态
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
//锁定引脚状态
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
\end{lstlisting}
同时HAL库帮我定义好了GPIO\_PIN\_RESET与GPIO\_PIN\_SET,代表着1(高电平)、0(低电平)。
\subsection{User Label}
对于任意引脚,它都有这么一个选项。我想告诉你这个选项特别特别好用!这个选项简单的说就是它帮你在main.h中生成define语句。但是对于HAL库编程,main.h会被用户的每个模块调用,也就是这些define语句的作用域几乎是全局。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{gpio_4.png}
\caption{Cube MX生成的define语句 \label{fig:scatter}}
\end{figure}
举个例子让你感受一下,在一次开发中,我使用PA0来作为输出引脚。如果随着开发的继续PA0被迫要用于其他功能,那么你该怎么办?那你必须使用另外一个引脚( 假设是PB1) 来替代它。
如果你没有配置User Label选项,那你的代码中可能大量的充斥着
\lstset{ language=C}
\begin{lstlisting}
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);//将PA0引脚状态改为低电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);//将PA0引脚状态改为高电平
\end{lstlisting}
然后你又需要用PB1来代替PA0,那你就需要将整个代码中有关PA0的GPIOA改成GPIOB,将GPIO\_PIN\_0改成GPIO\_PIN\_1。这会导致巨大的工作量,并且容易出错。
那么我们来看看使用了User Label会带来什么变化,使用User Label 把他取名R1。那你的代码中充斥着的不在是 HAL\_GPIO\_WritePin(GPIOA, GPIO\_PIN\_0, GPIO\_PIN\_RESET),而是HAL\_GPIO\_WritePin(R1\_GPIO\_Port, R1\_Pin, GPIO\_PIN\_RESET)。当遇到PA0被迫要用于其他功能,你只需要把PB1的User Label 取名为R1后,代码不需要做丝毫改变。
在我的开发中,这个应用最典型的两个例子就是“矩阵键盘”和“ADS1256”的开发。用矩阵键盘来举例,需要用到8个引脚。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{gpio_5.png}
\caption{Cube MX的配置\label{fig:scatter}}
\end{figure}
我的矩阵键盘中的代码全是由R1-R4、C1-C4组成,所以在各这个代码的复用性极其强,无论是换引脚还是换单片机型号,我只需要在Cube MX中配置一下,就可以马上投入使用。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{gpio_6.png}
\caption{矩阵键盘代码截图\label{fig:scatter}}
\end{figure}
\section{串口通信}
串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。
\subsection{UART 与 USART}
UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片,UART通常被集成于其他通讯接口的连结上。
USART:(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步串行接收/发送器,USART是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备。
\subsection{Cube MX相关配置}
\subsubsection{初始化引脚}
Mode :
Asynchronous : 异步, 整个过程,不会阻碍发送者的工作。
Synchronous : 同步, 同步信息一旦发送,发送者必须等到应答,才能继续后续的行为。
Single Wire : 单总线,半双工。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{uart_1.png}
\caption{使能引脚\label{fig:scatter}}
\end{figure}
\subsubsection{配置引脚}
Baud Rate: 波特率,波特率表示每秒钟传送的码元符号的个数,是衡量数据传送速率的指标,它用单位时间内载波调制状态改变的次数来表示。对于串口最重要的就是波特率,常用的波特率为115200与9600。
Wrod Length : 数据长
Parity : 奇偶校验 -> 无、奇校验、偶校验
Stop : 停止位
以上的配置与需要通信双方完全配对
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{uart_2.png}
\caption{配置引脚\label{fig:scatter}}
\end{figure}
\newpage
\subsection{编写逻辑代码}
\lstset{ language=C}
\begin{lstlisting}
//发送数据
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
//接收数据
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
//发送中断
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
//接收中断
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
//使用DMA发送
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
//使用DMA接收
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
//DMA暂停
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
//DMA恢复
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
//DMA停止
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);
\end{lstlisting}
就我目前的学习来看HAL并没有对同步通信的方式做拓展,所以上述都是关于UART的函数。
\subsection{printf重定向}
在Private includes中引入:
\begin{lstlisting}
#include <stdio.h>
\end{lstlisting}
在USER CODE BEGIN 0添加:
\lstset{ language=C}
\begin{lstlisting}
int fputc(int ch, FILE *f){
uint8_t temp[1] = {ch};
HAL_UART_Transmit(&huart1, temp, 1, 2);//huart1需要根据你的配置修改
return ch;
}
\end{lstlisting}
然后你就可以在任意地方使用printf语句方便的输出你想要的内容。
\subsection{Log信息格式}
\subsubsection{格式1}
参考目前主流嵌入式、安卓等输出方式:
\lstset{ language=C}
\begin{lstlisting}
[日志级别] 文件名 : 日志信息
//例:[info] main.c : init ok!
//例: [debug] adc.c : adc_getvalue -> 3.3v
\end{lstlisting}
\subsubsection{格式2}
参考Java日志框架的输出方式:
\lstset{ language=C}
\begin{lstlisting}
[ 文件名] 日志级别 : 日志信息
//例:[ main] info : init ok!
//例: [ adc] debug : adc_getvalue -> 3.3v
\end{lstlisting}
\subsection{条件编译}
说到这里我还想向大家介绍一下条件编译。因为在进行单片机开发的过程中,会需要大量的Log信息,但是在开发结束时,你又不想它一直打印( 这会拖慢单片机的速度 )。所以我提出我的办法:
在头文件中添加:
\lstset{ language=C}
\begin{lstlisting}
#define Log 1 // 打印Log信息,不想打印时改为0即可
\end{lstlisting}
再把.c文件中将所有的printf包裹上 \#if Log 与 \#endif:
\lstset{ language=C}
\begin{lstlisting}
#if Log
printf("[info]main.c:init!\r\n");
#endif
\end{lstlisting}
下面截选mppt算法中条件编译的使用:
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{uart_3.png}
\caption{条件编译在代码中的使用\label{fig:scatter}}
\end{figure}
\subsection{可变参数宏}
关于这个内容,是我在阅读国内某云物联网模块源码是发现并学习的。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{uart_4.png}
\caption{源码学习\label{fig:scatter}}
\end{figure}
我觉得这个解决方案比之前提到的条件编译强100倍,甚至让我感觉到以前的做法多么的愚蠢。这种方法不仅达到了代码的格式化,同时也完成了条件编译。
在此分享我的设计:
\begin{lstlisting}
#ifdef USER_MAIN_DEBUG
#define user_main_printf(format, ...) printf( format "\r\n", ##__VA_ARGS__)
#define user_main_info(format, ...) printf("[\tmain]info:" format "\r\n", ##__VA_ARGS__)
#define user_main_debug(format, ...) printf("[\tmain]debug:" format "\r\n", ##__VA_ARGS__)
#define user_main_error(format, ...) printf("[\tmain]error:" format "\r\n",##__VA_ARGS__)
#else
#define user_main_printf(format, ...)
#define user_main_info(format, ...)
#define user_main_debug(format, ...)
#define user_main_error(format, ...)
#endif
\end{lstlisting}
当我需要打印串口信息的时候,define一个USER\_MAIN\_DEBUG,在我不需要时将其注释。
\subsection{个性化输出}
1、借助下面的网站设计自己的字符
\href{http://patorjk.com/software/taag/}{http://patorjk.com/software/taag/}
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{uart_5.png}
\caption{字符组成的reyunn\label{fig:scatter}}
\end{figure}
2、编写代码
先逐行复制输入
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{uart_6.png}
\caption{逐行复制输入\label{fig:scatter}}
\end{figure}
再用转义字符修补错误
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{uart_7.png}
\caption{转义字符修补错误\label{fig:scatter}}
\end{figure}
3、串口助手看效果
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{uart_8.png}
\caption{打印信息效果\label{fig:scatter}}
\end{figure}
\subsection{串口中断}
1、Cube MX中开启中断
\begin{figure}[htbp]
\centering
\includegraphics[width=0.9\textwidth]{uart_9.png}
\caption{开启中断\label{fig:scatter}}
\end{figure}
2、在USER CODE BEGIN 2 中打开串口中断
\lstset{ language=C}
\begin{lstlisting}
HAL_UART_Receive_IT(&huart1, temp, 1);
\end{lstlisting}
3、在USER CODE BEGIN 4 中实现回调函数
\lstset{ language=C}
\begin{lstlisting}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart -> Instance == huart1.Instance ) {
...//业务代码
}
}
\end{lstlisting}
\section{外部中断}
外部中断是单片机实时地处理外部事件的一种内部机制。当某种外部事件发生时,单片机的中断系统将迫使CPU暂停正在执行的程序,转而去进行中断事件的处理;中断处理完毕后.又返回被中断的程序处,继续执行下去。
\subsection{Cube MX相关配置}
\subsubsection{初始化引脚}
如果你想使用PA1作为外部中断的接收引脚,那么你只需要点击PA1,在点击它对应的GPIO\_EXTIx
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{exti_1.png}
\caption{使能引脚\label{fig:scatter}}
\end{figure}
\subsubsection{使能中断}
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{exti_3.png}
\caption{使能中断\label{fig:scatter}}
\end{figure}
\newpage
\subsubsection{配置引脚}
这个地方与此前不同的地方在于GPIO mode。
External Interrupt Mode with Rising edge trigger detection//上升沿触发
External Interrupt Mode with Falling edge trigger detection//下降沿触发
External Interrupt Mode with Rising/Falling edge trigger detection//上升沿或下降沿触发
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{exti_2.png}
\caption{配置引脚\label{fig:scatter}}
\end{figure}
\subsection{编写逻辑代码}
在main.c中的USER CODE BEGIN 4编程范围内添加外部中断的回调函数:
\newpage
\lstset{ language=C}
\begin{lstlisting}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == PWM_Pin) {
...//业务代码
}
}
\end{lstlisting}
\subsection{测量pwm频率}
在我平时的学习中没有太多的使用外部中断,但是在最后的电赛中却巧妙的使用了它。
当时的情况是我们需要测量一个PWM的频率,我的解决办法是这样的:
当有上升沿的时候,就进入外部中断将pwm\_value的值+1。it is clear that "1s钟上升沿的次数就是pwm的频率"。所以当我要用pwm的频率时,我就先将pwm\_value置0,再延时1s,最后再使用pwm\_value。当然这并不是我最终的代码,因为你读到这里还有很多的内容没有学习,往后的定时器章节将介绍它的滤波算法。
\lstset{ language=C}
\begin{lstlisting}
int pwm_value =0 ;
int main(){
while (1){
pwm_value = 0; // pwm_value置0
HAL_Delay(1000); // 延时1s
printf("[\tmain]info:pwm_value=%d\r\n",pwm_value); // 读取pwm_value
}
}
/**
* @brief 外部中断的回调函数
* @param GPIO_Pin 触发中断的引脚
* @retval None
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == PWM_Pin) { // 判断触发引脚是否是定义的引脚
pwm_value++;
}
}
\end{lstlisting}
\newpage
\section{时钟树}
说到STM32,必然逃不开时钟树。但是时钟树要展开讲的话会很麻烦,而且我也不一定讲的好。但是我想告诉你的是:通常我们会让单片机的频率( 决定单片机的处理速度)提到最大,再进行其他分频操作。
原谅我技术有限,所以我想分享的关于时钟树的就是小小的一点:
\subsection{使能外部时钟源}
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{rcc1.png}
\caption{使能外部时钟源 \label{fig:scatter}}
\end{figure}
\subsection{将频率调至最大}
不同单片机的最大运行频率是不同的,例如stm32f103为72M而stm32f407为84M。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{rcc2.png}
\caption{将频率调至最大 \label{fig:scatter}}
\end{figure}
\subsection{按需分频}
\begin{figure}[htbp]
\centering
\includegraphics[width=0.9\textwidth]{rcc3.png}
\caption{按需分频 \label{fig:scatter}}
\end{figure}
\section{定时器}
分享完时钟树的部分,接下来就是和它最紧密的定时器了。定时器最基本的内容就是定时产生中断了:
\subsection{Cube MX相关配置}
\subsubsection{配置定时器时钟}
如之前所示,将定时器的时钟设为72M。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.8\textwidth]{tim_1.png}
\caption{配置定时器频率 \label{fig:scatter}}
\end{figure}
\subsubsection{选择时钟源}
选择内部时钟
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{tim_2.png}
\caption{选择时钟源 \label{fig:scatter}}
\end{figure}
\subsubsection{配置定时器}
定时器的配置主要有两个:定时时间与是否重装定时器。
定时频率 = 定时器时钟 / ( 预分频 +1) /( 计数值 + 1 ) Hz。
定时时间 = 1 / 定时频率 s。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{tim_3.png}
\caption{配置定时器 \label{fig:scatter}}
\end{figure}
\subsubsection{开启中断 - 基本定时器}
勾选Enabled框即可。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{tim_4.png}
\caption{开启中断 \label{fig:scatter}}
\end{figure}
\newpage
\subsubsection{开启中断 - 高级定时器}
勾选TIM X update interrupt 后的 Enabled框即可。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{tim_5.png}
\caption{开启中断 \label{fig:scatter}}
\end{figure}
\subsection{编写业务代码}
\lstset{language=C}
\begin{lstlisting}
int main(){
HAL_TIM_Base_Start_IT(&htim1); //定时器1使能
HAL_TIM_Base_Start_IT(&htim2); //定时器2使能
...
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == htim1.Instance) {
...//定时器1中断业务
}
else if(htim-> Instance == htim2.Instance) {
...//定时器2中断业务
}
...
}
\end{lstlisting}
\subsection{平滑滤波}
在这里我想在介绍定时器的另一种用法:平滑滤波。绝大部分人的滤波算法都是用的时候,多次采样再滤波。但是我希望让采样值在另一个“线程”一直滤波,而在我需要他的时候,直接取它的值即可。记得之前我描述过用外部中断实现的测量pwm波的频率,接下我想分享一下用定时器对其进行滤波。
\lstset{language=C}
\begin{lstlisting}
/* 定时器2配置为0.1s触发一次中断 */
/**
* @brief 定时器中断的回调函数
* @param htim 触发中断的定时器
* @retval None
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim-> Instance == htim2.Instance) {
pwm_sum += pwm_value * 10; //pwm_sum累加
pwm_sum -= pwm_avg; //pwm_sum减去上次的平均值
pwm_avg = pwm_sum * 1.0 / 5; //更新pwm的平均值
pwm_value_final = pwm_avg; //pwm_value_final的值即为当前pwm的频率
pwm_value = 0; //将pwm_value清空,重新计数
}
}
/**
* @brief 外部中断的回调函数
* @param GPIO_Pin 触发中断的引脚
* @retval None
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == PWM_Pin) { // 判断触发引脚是否是定义的引脚
pwm_value++;
}
}
\end{lstlisting}
当我们在任意时刻需要使用pwm的频率时,只需要使用pwm\_value\_final的值即可。
\section{PWM/SPWM}
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调试。是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。广泛应用在从测量、通信到功率控制与变换的许多领域中。
SPWM(Sinusoidal PWM)法是一种比较成熟的、使用较广泛的PWM法。冲量相等而形状不同的窄脉冲加在具有惯性的环节上时,其效果基本相同。SPWM法就是以该结论为理论基础,用脉冲宽度按正弦规律变化而和正弦波等效的PWM波形即SPWM波形控制逆变电路中开关器件的通断,使其输出的脉冲电压的面积与所希望输出的正弦波在相应区间内的面积相等,通过改变调制波的频率和幅值则可调节逆变电路输出电压的频率和幅值。
PWM和SPWM在电源的备战中是很有必要的。基础的恒流源、恒压源需要使用PWM的占空比及频率来达到数控的作用,往后的逆变则需要用到SPWM。那我就先从简单的PWM做分享,PWM输出其实是定时器的一种应用。那么配置定时器时钟与选择时钟源我就不再赘述了。就从使能PWM通道开始讲起。
\subsection{Cube MX相关配置-PWM}
\subsubsection{使能PWM通道}
在这里我将TIM2的Channel1设置为PWM输出通道(PWM Generation CHx正向 、PWM Generation CHxN反向 、 PWM Generation CHx CHxN一对互补pwm输出)
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{pwm_1.png}
\caption{使能PWM通道 \label{fig:scatter}}
\end{figure}
\subsubsection{配置频率及占空比}
频率 = 定时器时钟 / ( Prescaler预分频 + 1) / ( Counter Period计数值 + 1) Hz
占空比 = Pulse ( 对比值 ) / ( C ounter Period计数值 ) \%
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{pwm_2.png}
\caption{配置频率及占空比 \label{fig:scatter}}
\end{figure}
\subsection{编写业务代码-PWM}
\lstset{language=C}
\begin{lstlisting}
// 使能timx的通道y
HAL_TIM_PWM_Start(&htimx,TIM_CHANNEL_y);
// 修改timx的通道y的pwm比较值为z,即修改占空比
__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, z);
\end{lstlisting}
pwm的输出是很简单的,但是因为定时器的频率是有上限的通常需要在频率和pwm的精细度两者之间做取舍。所以你想做电源,那么你可以了解一下STM32F334这款处理器,它拥有一个高分辨率定时器(HRTIM),能将定时器的频率倍频至4.096G。那你在频率和pwm的精细度两者都可以兼得。
\emph{SPWM其实就是在PWM的基础上,让PWM的占空比做正弦变化。}
\subsection{Cube MX相关配置-SPWM}
之前的PWM生成的操作不变,只需要开启一个新的定时器,配置完后需要开启定时器中断
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{spwm_1.png}
\caption{开启一个新的定时器 \label{fig:scatter}}
\end{figure}
\newpage
\subsection{使用软件生成正弦向量表-SPWM}
SPWM 中值 = Pulse ( 对比值 ) /2
SPWM 幅值 = Pulse ( 对比值 ) /2
周内点数影响频率与正弦波精细度。周内点数越大,频率越小、正弦波精细度越高。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.6\textwidth]{spwm_2.png}
\caption{使用软件生成正弦向量表 \label{fig:scatter}}
\end{figure}
\subsection{编写业务代码-SPWM}
\lstset{language=C}
\begin{lstlisting}
uint16_t sin[] = {
1800,1913,2025,2137,2247,2356,2462,2566,2667,2764,
2858,2947,3032,3112,3186,3256,3319,3377,3428,3473,
3511,3543,3568,3585,3596,3600,3596,3585,3568,3543,
3511,3473,3428,3377,3319,3256,3186,3112,3032,2947,
2858,2764,2667,2566,2462,2356,2247,2137,2025,1913,
1800,1686,1574,1462,1352,1243,1137,1033,932,835,
741,652,567,487,413,343,280,222,171,126,
88,56,31,14,3,0,3,14,31,56,
88,126,171,222,280,343,413,487,567,652,
741,835,932,1033,1137,1243,1352,1462,1574,1686
}
int main(){
HAL_TIM_PWM_Start(&htimx,TIM_CHANNEL_y); // 开启pwm输出
HAL_TIM_Base_Start_IT(&htimz); //使能刚刚配置的定时器z
while(1){
}
}
/**
* @brief 定时器中断的回调函数
* @param htim 触发中断的定时器
* @retval None
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
static int i = 0;
if(++i == size)i = 0;
if (htim->Instance == htim3.Instance){
__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, sin[i]); //由向量表修改占空比
}
}
\end{lstlisting}
对于做电源的同学来说,这个是必须要掌握的内容!
\section{ADC / SDADC / ADS 模数转化}
先介绍最简单的片上ADC,通常是12位,精度则为3.3/4096 v。
读取ADC的方式有很多:
1、轮询
2、中断
3、DMA
因为在实际开发中仅有轮询和DMA存在使用场景,所以在这里我仅介绍轮询和DMA的方式。
\subsection{Cube MX相关配置-轮询方式}
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{adc_1.png}
\caption{使能ADC引脚 \label{fig:scatter}}
\end{figure}
\subsection{编写业务代码-轮询方式}
\lstset{language=C}
\begin{lstlisting}
while(1){
HAL_ADC_Start(&hadc1);//启动ADC装换
HAL_ADC_PollForConversion(&hadc1, 50);//等待转换完成,第二个参数表示超时时间,单位ms.
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC)){
AD_Value = HAL_ADC_GetValue(&hadc1);//读取ADC转换数据,数据为12位
printf("[\tmain]info:v=%.1fmv\r\n",AD_Value*3300.0/4096);//打印日志
}
}
\end{lstlisting}
前面介绍了通过ADC轮询的方式采集单通道的数据。现在介绍一下通过DMA方式采集多通道的数据。
\subsection{Cube MX相关配置-DMA方式}
\subsubsection{初始化两个ADC通道}
\newpage
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{adc_2.png}
\caption{初始化两个ADC通道 \label{fig:scatter}}
\end{figure}
\subsubsection{配置相关属性}
step 1 : 使能扫描转换模式(Scan Conversion Mode),使能连续转换模式(Continuous Conversion Mode)。
step 2 : ADC规则组选择转换通道数为2(Number Of Conversion)。
step 3 : 配置Rank的输入通道。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{adc_3.png}
\caption{配置相关属性 \label{fig:scatter}}
\end{figure}
\subsubsection{添加DMA}
添加DMA设置,设置为连续传输模式,数据长度为字。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{adc_4.png}
\caption{添加DMA \label{fig:scatter}}
\end{figure}
\newpage
\subsection{编写业务代码-DMA方式}
1、在main函数前面添加变量。其中ADC\_Value作为转换数据缓存数组,ad1,ad2存储PA0(转换通道0),PA1(转换通道1)的电压值。
\begin{lstlisting}
/* USER CODE BEGIN PV */
/* Private variables */
uint32_t ADC_Value[100];
uint8_t i;
uint32_t ad1,ad2;
/* USER CODE END PV */
\end{lstlisting}
2、在while(1)前面以DMA方式开启ADC装换。HAL\_ADC\_Start\_DMA()函数第二个参数为数据存储起始地址,第三个参数为DMA传输数据的长度。
\begin{lstlisting}
/* USER CODE BEGIN 2 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&ADC_Value, 100);
/* USER CODE END 2 */
\end{lstlisting}
由于DMA采用了连续传输的模式,ADC采集到的数据会不断传到到存储器中(此处即为数组ADC\_Value)。ADC采集的数据从ADC\_Value[0]一直存储到ADC\_Value[99],然后采集到的数据又重新存储到ADC\_Value[0],一直到ADC\_Value[99]。所以ADC\_Value数组里面的数据会不断被刷新。这个过程中是通过DMA控制的,不需要CPU参与。我们只需读取ADC\_Value里面的数据即可得到ADC采集到的数据。
其中ADC\_Value[0]为通道0(PA0)采集的数据,ADC\_Value[1]为通道1(PA1)采集的数据,ADC\_Value[2]为通道0采集的数据,如此类推。数组偶数下标的数据为通道0采集数据,数组奇数下标的数据为通道1采集数据。
在while(1)循环中添加应用程序,将采集的数据装换为电压值并输出。
\begin{lstlisting}
/* USER CODE BEGIN WHILE */
while (1){
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_Delay(500);
for(i = 0,ad1 =0,ad2=0; i < 100;){
ad1 += ADC_Value[i++];
ad2 += ADC_Value[i++];
}
ad1 /= 50;
ad2 /= 50;
printf("\r\n********ADC-DMA-Example********\r\n");
printf("[\tmain]info:AD1_value=%1.3fV\r\n", ad1*3.3f/4096);
printf("[\tmain]info:AD2_value=%1.3fV\r\n", ad2*3.3f/4096);
}
/* USER CODE END 3 */
\end{lstlisting}
程序中将数组偶数下标数据加起来求平均值,实现均值滤波的功能,再将数据装换为电压值,即为PA0管脚的电压值。同理对数组奇数下标数据处理得到PA1管脚的电压值。
\emph{同时ADC采样也可以采用我之前描述的采用定时器对其平滑滤波!}
通常片上的ADC的精度往往达不到我们的要求,因为它的精度实在是太低了。有两个替代方案:
1、SDADC,这个是STM32F373上特有的功能,16位高速ADC,支持差分输入。掌握难度较大,我也没有很好的掌握,所以就不在此展示了。
2、ADS,就是外置ADC。在我们比赛前,我们一直调教的是ADS1256这款芯片,能做到0.01mV的精度!这类芯片只需要进行SPI通信操作,便可以获取ADC数据。
\section{DAC 数模转化}
说实话,这两年的开发中,我还没有使用过DAC的功能。但是这个功能也十分简单,配置好引脚后,编写业务代码即可。
\subsection{Cube MX相关配置}
勾选DAC中的OUT Configuration,其余配置为默认配置不需修改。
\begin{figure}[htbp]
\centering
\includegraphics[width=1.0\textwidth]{dac_1.png}
\caption{Cube MX相关配置 \label{fig:scatter}}
\end{figure}
\newpage
\subsection{编写业务代码}