頭文件組織與模塊化封裝:從LED到綜合項(xiàng)目工程實(shí)踐)
簡介面向51單片機(jī)初學(xué)者與開發(fā)者的模塊化C語言頭文件合集涵蓋輸入輸出端口、定時(shí)器與計(jì)數(shù)器、中斷系統(tǒng)、串行通信、模數(shù)轉(zhuǎn)換器、存儲器管理、看門狗及電源管理等核心外設(shè)將底層寄存器操作封裝成簡潔函數(shù)接口使開發(fā)者能集中精力理解模塊功能而不是糾結(jié)硬件細(xì)節(jié)。壓縮包內(nèi)共十一個(gè)文件其中八個(gè)頭文件覆蓋溫度傳感器、I2C總線、液晶顯示、實(shí)時(shí)時(shí)鐘、移位寄存器、鍵盤、鎖存器及全局配置等典型模塊另有三個(gè)文本說明文件對定時(shí)器與中斷、液晶常用指令、頭文件作用進(jìn)行補(bǔ)充解釋整體僅10KB輕量易用適合快速查閱與移植。目前已有1025人學(xué)習(xí)下載適用于課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)及入門自學(xué)。借助這套頭文件讀者可以迅速搭建起51單片機(jī)的基礎(chǔ)系統(tǒng)同時(shí)從函數(shù)聲明、宏定義、數(shù)據(jù)結(jié)構(gòu)的組織方式中學(xué)習(xí)模塊化編程和代碼復(fù)用技巧避免重復(fù)勞動為后續(xù)擴(kuò)展步進(jìn)電機(jī)控制、傳感器采集、無線通信等復(fù)雜應(yīng)用提供清晰框架。 玩51單片機(jī)這件事很多人都是從點(diǎn)亮一顆LED開始的。我當(dāng)時(shí)也是照著教程把代碼敲進(jìn)Keil下載到板子上燈亮了那一刻確實(shí)有成就感。但等我把數(shù)碼管、LCD1602、舵機(jī)、串口一個(gè)個(gè)玩下來才發(fā)現(xiàn)真正決定一個(gè)51工程好不好維護(hù)的不是某個(gè)功能多花哨而是頭文件組織得夠不夠清爽。這篇文章就把我在51開發(fā)里攢下的各模塊頭文件寫法、封裝思路和踩坑經(jīng)驗(yàn)一次性整理出來。不管你是剛把GPIO搞明白的新手還是已經(jīng)在做智能小車、電子時(shí)鐘、溫控風(fēng)扇這類綜合項(xiàng)目的老手這篇文章都能讓你少走不少彎路。內(nèi)容圍繞“頭文件”這個(gè)核心覆蓋LED、按鍵、數(shù)碼管、LCD1602、串口、定時(shí)器、DS18B20溫度采集等常用模塊講清楚每個(gè)文件的邊界、該放什么、不該放什么以及多模塊工程怎么組織才不亂。1. 搞懂頭文件之前先看這三個(gè)問題1.1 沒有頭文件的工程會亂成什么樣先說實(shí)話一個(gè)純51小項(xiàng)目幾百行代碼悶頭寫在一個(gè)main.c里不是不能用。但如果項(xiàng)目里同時(shí)有LCD1602、DS18B20、按鍵掃描、串口打印、舵機(jī)PWM全堆在一個(gè)文件里你就得面對這幾個(gè)問題第一改一個(gè)引腳定義可能要在上千行里搜索好幾次改漏一個(gè)就是硬件上的低級故障第二變量滿天飛main函數(shù)里隨便定義幾十個(gè)全局變量查起bug來像大海撈針第三想復(fù)用某個(gè)模塊只能“復(fù)制代碼再刪刪改改”稍不留神就帶過來一堆無關(guān)的東西。頭文件解決的就是這三件事隔離、聲明、復(fù)用。用生活里的例子類比頭文件就是設(shè)備的產(chǎn)品說明書——告訴你這個(gè)模塊對外提供哪些接口函數(shù)、宏定義但具體內(nèi)部怎么實(shí)現(xiàn)跟你沒直接關(guān)系。你的main.c只需要“看說明書調(diào)用”不用關(guān)心LCD1602底層時(shí)序到底是怎么拉的。1.2 頭文件里該放什么不該放什么很多初學(xué)者會有誤解覺得頭文件就是把代碼貼進(jìn)去而且貼得越多越“完整”。實(shí)際上頭文件應(yīng)該保持精簡核心內(nèi)容是“對外暴露的接口聲明”不是“實(shí)現(xiàn)細(xì)節(jié)”。一般來說一個(gè)頭文件包括這幾樣防重復(fù)包含的宏保護(hù)、依賴的頭文件、引腳和參數(shù)宏定義、數(shù)據(jù)類型重定義、函數(shù)聲明、需要跨文件訪問的全局變量extern聲明。不該放進(jìn)頭文件的也明確一下函數(shù)體不要放除非是static inline這種特殊寫法、變量定義不要放、局部用的輔助宏不要放。尤其容易翻車的是“在頭文件里定義變量”比如直接寫unsigned char flag 0;只要這個(gè)頭文件被兩個(gè).c文件包含鏈接階段大概率直接報(bào)重復(fù)定義錯(cuò)誤。如果你確實(shí)需要一個(gè)跨文件的全局標(biāo)志位正確做法是在某個(gè).c文件里定義變量在頭文件里用extern unsigned char flag;聲明它。我用一個(gè)標(biāo)準(zhǔn)模板來說明頭文件的基本骨架#ifndef __LED_H__ #define __LED_H__ #include reg52.h // 引腳映射 sbit LED1 P1^0; sbit LED2 P1^1; // 對外函數(shù)聲明 void LED_Init(void); void LED_On(unsigned char num); void LED_Off(unsigned char num); void LED_Toggle(unsigned char num); #endif注意宏保護(hù)#ifndef __LED_H__這個(gè)不能省后面單獨(dú)講。1.3 宏保護(hù)不加真的會翻車假設(shè)你有兩個(gè)頭文件a.h里包含了common.hb.h里也包含了common.h然后在某段代碼里同時(shí)包含a.h和b.h那common.h的內(nèi)容就被編譯器處理了兩遍。如果common.h里有宏定義、類型定義這類內(nèi)容重復(fù)處理輕則報(bào)警告重則直接報(bào)“redefinition重復(fù)定義”錯(cuò)誤而且這種錯(cuò)誤最難查——你明明沒寫重復(fù)代碼編譯器卻告訴你重復(fù)了。解決辦法就是給每個(gè)頭文件加宏保護(hù)。原理很簡單第一次處理該頭文件時(shí)宏__XXX_H__還沒定義編譯器正常處理并在開頭把它定義下來第二次再遇到這個(gè)文件時(shí)因?yàn)楹暌呀?jīng)存在#ifndef條件不成立整個(gè)內(nèi)容被跳過。這樣不管一個(gè)頭文件被間接包含了多少次實(shí)際內(nèi)容只會在編譯時(shí)被展開一次。宏保護(hù)還有一種老寫法是#pragma once用起來更省事。但Keil C51對#pragma once的支持不如#ifndef那么穩(wěn)我個(gè)人的習(xí)慣是老老實(shí)實(shí)寫#ifndef / #define / #endif跨編譯器移植零風(fēng)險(xiǎn)這也是我推薦給入門用戶的做法。2. 六大常用模塊頭文件直接照著寫就行2.1 LED、按鍵、蜂鳴器這類GPIO外設(shè)怎么封裝GPIO類外設(shè)是51最基礎(chǔ)的東西但它頭文件的寫法直接影響你后面做項(xiàng)目的效率。以LED為例我習(xí)慣把每個(gè)LED的引腳用sbit定義在頭文件里再提供初始化、點(diǎn)亮、熄滅、翻轉(zhuǎn)四個(gè)函數(shù)。這樣main.c里不會出現(xiàn)LED1 0這種裸操作所有邏輯都通過函數(shù)名來表達(dá)代碼讀起來和英文句子差不多。按鍵模塊稍微復(fù)雜一點(diǎn)因?yàn)橛邢丁n^文件里我一般這樣設(shè)計(jì)#ifndef __KEY_H__ #define __KEY_H__ #include reg52.h #define KEY_DETECT_DELAY_MS 10 sbit KEY_UP P3^2; sbit KEY_DOWN P3^3; typedef enum { KEY_NONE 0, KEY_UP_PRESSED, KEY_DOWN_PRESSED } KeyEvent; KeyEvent KEY_Scan(void); #endif這里把按鍵事件定義成枚舉返回值能表達(dá)“沒有按鍵/哪個(gè)按鍵被按下”這幾種狀態(tài)比返回單個(gè)位狀態(tài)更清晰。實(shí)際掃描函數(shù)在.c文件里實(shí)現(xiàn)按鍵那邊加個(gè)狀態(tài)鎖存避免一次按下被多次觸發(fā)。這類GPIO外設(shè)封裝的共同點(diǎn)是所有硬件細(xì)節(jié)哪個(gè)引腳、高有效還是低有效都收在頭文件里業(yè)務(wù)層只關(guān)心邏輯狀態(tài)。后面你要是換板子只需改頭文件里的sbit定義main.c一行不用動。2.2 數(shù)碼管段碼表和動態(tài)掃描的頭文件組織數(shù)碼管的頭文件有它自己的特殊性核心難點(diǎn)是段碼表放哪。段碼表按共陰共陽來分是兩份完全不同的數(shù)據(jù)0~F的數(shù)字段碼基本是固定的我建議把段碼表定義在.c文件里用code關(guān)鍵字修飾讓它存到程序存儲器而不是占用寶貴的RAM。頭文件里可以用宏來做共陰共陽的切換也可以用條件編譯來管理#ifndef __SEG_H__ #define __SEG_H__ #include reg52.h // 共陰數(shù)碼管段碼表0 1 2 3 4 5 6 7 8 9 A b C d E F - extern unsigned char code SEG_Code[]; // 位選引腳 sbit SEG_WE1 P2^0; sbit SEG_WE2 P2^1; sbit SEG_WE3 P2^2; sbit SEG_WE4 P2^3; void SEG_Init(void); void SEG_Display(unsigned char pos, unsigned char num); void SEG_Scan(void); #endif動態(tài)掃描是用視覺暫留效應(yīng)讓4位數(shù)碼管同時(shí)“看起來在亮”核心是刷新頻率和消隱。刷新太快人眼會有亮度不均太慢會看到閃爍實(shí)測5ms左右刷新一個(gè)位是比較舒服的區(qū)間。還有個(gè)小細(xì)節(jié)每次切換位選前一定要先關(guān)閉所有位選也就是消隱否則會出現(xiàn)拖影、鬼影現(xiàn)象。這個(gè)坑初學(xué)者特別容易踩我早期做電子時(shí)鐘時(shí)數(shù)碼管數(shù)字邊緣總是有一圈淡淡的殘影后來才發(fā)現(xiàn)就是切換時(shí)沒消隱。2.3 LCD1602顯示模塊的完整聲明LCD1602大概是51項(xiàng)目里出現(xiàn)頻率最高的顯示器件16個(gè)字符×2行接4線或8線模式控制全靠那幾條時(shí)序線。它的頭文件里我習(xí)慣把命令字用宏定義列出來比如清屏命令0x01、光標(biāo)歸位0x02、顯示開關(guān)0x0C等這樣后續(xù)寫顯示邏輯時(shí)一眼就能看懂。#ifndef __LCD1602_H__ #define __LCD1602_H__ #include reg52.h #define LCD_CLEAR 0x01 #define LCD_CURSOR_HOME 0x02 #define LCD_DISPLAY_ON 0x0C #define LCD_FUNCTION_SET 0x28 sbit LCD_RS P2^5; sbit LCD_RW P2^6; sbit LCD_EN P2^7; #define LCD_DATA P0 void LCD_Init(void); void LCD_WriteCmd(unsigned char cmd); void LCD_WriteData(unsigned char dat); void LCD_SetCursor(unsigned char row, unsigned char col); void LCD_PrintString(char *str); void LCD_PrintChar(char ch); #endif寫LCD1602程序最核心的是時(shí)序RS決定是命令還是數(shù)據(jù)RW決定讀還是寫EN需要一個(gè)從高到低的下降沿來鎖存數(shù)據(jù)。實(shí)際項(xiàng)目里絕大多數(shù)場景只寫不讀所以RW直接拉低就行這樣能省一條IO。我在頭文件里特意不把具體初始化序列展開而是在.c里實(shí)現(xiàn)因?yàn)槌跏蓟蛄邪訒r(shí)和若干條命令屬于“實(shí)現(xiàn)細(xì)節(jié)”不需要對外暴露。有一點(diǎn)提醒LCD1602的LCD_DATA用P0整口很多新手喜歡定義成sbit D0 P0^0這種寫法結(jié)果一個(gè)字節(jié)要搬8次代碼啰嗦還容易錯(cuò)。用#define LCD_DATA P0后直接LCD_DATA 0x38就能一次送出命令數(shù)據(jù)省事得多。2.4 串口通信頭文件與波特率計(jì)算串口是51和PC、藍(lán)牙模塊、GPS模塊通信的標(biāo)配它的頭文件除了函數(shù)聲明外最好把波特率相關(guān)的參數(shù)也集中在一起方便不同項(xiàng)目快速切換。串口工作方式1是最常用的波特率由定時(shí)器1的溢出率決定公式是波特率 (2^SMOD / 32) × (定時(shí)器溢出頻率)。實(shí)際工程中我們反著用先確定目標(biāo)波特率再算定時(shí)器初值。以11.0592MHz晶振、波特率9600、SMOD0為例機(jī)器周期是12/11.0592MHz定時(shí)器1溢出率是 11.0592MHz / 12 / (256 - TH1)。要讓波特率等于9600就得讓溢出率等于9600×32307200因此256-TH1 11.0592MHz / 12 / 307200 ≈ 3TH1初值就是253。這也是為什么幾乎所有51教程都推薦11.0592MHz晶振——它能精確算出9600波特率而12MHz晶振怎么算都帶小數(shù)誤差。串口頭文件我通常會帶上接收緩沖區(qū)的外部聲明#ifndef __UART_H__ #define __UART_H__ #include reg52.h #define BUF_SIZE 32 extern unsigned char uart_rx_buf[BUF_SIZE]; extern unsigned char uart_rx_cnt; void UART_Init(unsigned int baud); void UART_SendByte(unsigned char dat); void UART_SendString(char *str); void UART_ISR(void); #endif注意uart_rx_buf是用extern聲明的實(shí)際定義放在uart.c里初始化時(shí)清零。這樣在main.c里可以直接遍歷緩沖區(qū)處理接收到的命令適合做串口控制小車、串口調(diào)參這類功能。2.5 定時(shí)器與舵機(jī)PWM控制的關(guān)鍵參數(shù)定時(shí)器這塊頭文件里主要是把初值計(jì)算用的宏定義出來。工作方式1是16位計(jì)數(shù)器初值公式是初值 65536 - 需要計(jì)數(shù)的時(shí)間 / 機(jī)器周期。以12MHz晶振、定時(shí)器0產(chǎn)生1ms中斷為例機(jī)器周期是1μs初值 65536 - 1000 64536也就是TH0 0xFC; TL0 0x18;。這里有個(gè)容易被忽略的坑如果晶振換成11.0592MHz同樣的初值將產(chǎn)生約1.085ms的中斷你算出來的延時(shí)就不是整1ms了。所以頭文件里最好用宏把晶振頻率和初值寫清楚方便整體調(diào)整。舵機(jī)控制本質(zhì)上也是定時(shí)器的活——標(biāo)準(zhǔn)的模擬舵機(jī)要求20ms的周期信號高電平脈寬在0.5ms到2.5ms之間對應(yīng)0度到180度。用定時(shí)器產(chǎn)生PWM的思路是一個(gè)1ms的時(shí)基中斷外加一個(gè)脈寬計(jì)數(shù)器在20ms的周期里根據(jù)目標(biāo)角度控制輸出電平的翻轉(zhuǎn)點(diǎn)。我在頭文件里習(xí)慣把角度和脈寬的映射關(guān)系做成宏#define SERVO_PERIOD_MS 20 #define SERVO_MIN_PULSE 500 #define SERVO_MAX_PULSE 2500 #define SERVO_MID_PULSE 1500這樣在.c里寫SERVO_MIN_PULSE、SERVO_MAX_PULSE代碼可讀性高調(diào)試時(shí)也容易確認(rèn)上下限。實(shí)際控制時(shí)脈寬直接對應(yīng)高電平時(shí)間比如要讓舵機(jī)轉(zhuǎn)到90度就把高電平保持1.5ms剩下的18.5ms維持低電平。3. 多模塊工程實(shí)戰(zhàn)目錄這樣組織才不亂3.1 一套能直接用的51工程目錄與Include Path配置當(dāng)你開始做綜合項(xiàng)目比如智能小車、溫控風(fēng)扇、倒車?yán)走_(dá)頭文件的組織就不再是單個(gè)文件的事而是整個(gè)工程的骨架。我常用的目錄結(jié)構(gòu)是“分層”思路按驅(qū)動、中間層、應(yīng)用來劃分用Keil C51建工程時(shí)大概長這樣project/ ├── user/ │ ├── main.c │ └── config.h ├── driver/ │ ├── led.c / led.h │ ├── key.c / key.h │ ├── seg.c / seg.h │ ├── lcd1602.c / lcd1602.h │ ├── uart.c / uart.h │ └── timer.c / timer.h └── module/ ├── ds18b20.c / ds18b20.h └── servo.c / servo.huser目錄放main.c和全局配置頭文件driver目錄放芯片外設(shè)驅(qū)動module目錄放基于驅(qū)動封裝的業(yè)務(wù)模塊。這樣分層的好處是——驅(qū)動層不知道業(yè)務(wù)邏輯模塊層只調(diào)用驅(qū)動main.c只寫業(yè)務(wù)擴(kuò)展邏輯。做溫控風(fēng)扇時(shí)溫度采集和風(fēng)扇控制是module層的事main.c里只需要初始化、然后在一個(gè)循環(huán)里讀取溫度并更新風(fēng)扇檔位。Keil里有一個(gè)非常容易踩的坑頭文件分散在多個(gè)目錄后編譯會報(bào)“找不到頭文件”。這不是你代碼有問題而是Include Path沒配置。打開Options for Target - C/C - Include Paths把driver和module這些目錄都加進(jìn)去編譯器才找得到#include led.h這種用戶頭文件。另外提醒一句用戶自定義頭文件用#include xxx.hKeil編譯器的搜索順序是先在當(dāng)前文件所在目錄找再去Include Path里找。如果你用#include xxx.h編譯器會直接去標(biāo)準(zhǔn)頭文件目錄找自定義頭文件很可能找不到。3.2 全局配置頭文件一個(gè)宏控制整個(gè)工程多模塊工程里我最推薦的一個(gè)文件就是全局配置頭文件一般叫config.h或board.h。它的作用是把所有跟硬件平臺相關(guān)的“可變項(xiàng)”集中到一起換板子時(shí)只改這一個(gè)文件。常見的做法是用條件編譯宏來控制模塊開關(guān)。比如你的工程板子上沒焊LCD1602在config.h里就不定義USE_LCD1602同時(shí)main.c和模塊文件里用#ifdef USE_LCD1602包住相關(guān)代碼這樣編譯出來的HEX文件會自動把LCD1602相關(guān)代碼排除。對51這種小容量單片機(jī)來說這個(gè)特性可以幫你省下不少程序空間也讓工程在不同配置的板子間輕松切換。config.h里還適合放晶振頻率宏、數(shù)據(jù)類型重定義、常用延時(shí)參數(shù)。比如#ifndef __CONFIG_H__ #define __CONFIG_H__ #define FOSC 11059200UL #define UART_BAUD 9600 #define TICK_MS 1 typedef unsigned char u8; typedef unsigned int u16; typedef unsigned long u32; #define USE_LCD1602 // #define USE_SERVO #endif這里用typedef定義u8/u16/u32在很多51代碼里已經(jīng)算是約定俗成了寫起來比unsigned char短而且移植到ARM等平臺時(shí)只需改這里。但要注意這個(gè)config.h應(yīng)該被所有.c文件第一個(gè)包含而且放宏保護(hù)否則重復(fù)包含又會出現(xiàn)之前說的redefinition問題。4. 編譯報(bào)錯(cuò)排查與避坑經(jīng)驗(yàn)實(shí)錄4.1 新手最常踩的四個(gè)編譯錯(cuò)誤頭文件相關(guān)的編譯錯(cuò)誤翻來覆去其實(shí)就那么幾類我把最常見的整理成表格方便大家對照錯(cuò)誤信息常見形式常見原因解決辦法undefined identifier調(diào)用函數(shù)/變量前沒有找到聲明檢查是否包含對應(yīng)頭文件、函數(shù)名是否拼寫一致redefinition同一類型/宏被定義了多次檢查頭文件宏保護(hù)是否寫好、是否重復(fù)包含MULTIPLE PUBLIC DEFINITIONS變量在頭文件里直接定義了把變量定義移到.c文件頭文件里只留extern聲明*** FATAL ERROR L210: I/O ERROR編譯器找不到頭文件路徑檢查Include Path配置是否正確除了表格里的這幾類還有一個(gè)經(jīng)典問題是“函數(shù)聲明了但沒實(shí)現(xiàn)”編譯會報(bào)undefined symbol。這個(gè)一般出現(xiàn)在你只把頭文件加進(jìn)工程、但對應(yīng)的.c文件沒有加入Keil的Source Group里。記住在Keil里頭文件只是聲明真正讓函數(shù)有實(shí)體的是.c文件加入工程并參與編譯。我見過很多次有人在文件管理器里刪了.c文件只留.h然后編譯怎么都過不去。4.2 頭文件和芯片型號對不上代碼白寫51并不是只有AT89C52這一種芯片STC89C52、STC12C5A60S2、STC15W408AS、STC8系列這些都是51內(nèi)核但特殊功能寄存器SFR的地址和數(shù)量并不完全一樣。reg52.h是針對早期8052芯片的你如果拿它去編譯STC8的定時(shí)器、ADC相關(guān)代碼編譯器會告訴你“undefined identifier”或者直接寄存器沒定義。解決這個(gè)問題有幾個(gè)辦法第一用STC官方提供的STC-ISP軟件它可以根據(jù)你選的芯片型號直接生成對應(yīng)的頭文件里面定義了該型號全部SFR和SFR位第二在官方頭文件基礎(chǔ)上把你用到的額外寄存器用sfr、sbit自行補(bǔ)充定義到工程頭文件里。比如STC12C5A60S2的ADC相關(guān)寄存器在reg52.h里就沒有你就要自己加sfr ADC_CONTR 0xBC; sfr ADC_RES 0xBD; sfr ADC_RESL 0xBE;我自己的習(xí)慣是直接使用官方生成的頭文件作為模板再在其中挑出項(xiàng)目用到的部分保留把用不到的注釋掉最后統(tǒng)一用#include包含。這樣既保證硬件寄存器定義正確又節(jié)省編譯時(shí)間。4.3 一個(gè)關(guān)于sizeof和頭文件的常見誤解最后聊一個(gè)搜“頭文件”時(shí)經(jīng)常冒出來的誤區(qū)有人問sizeof函數(shù)需要什么頭文件。這里明確說sizeof不是函數(shù)是C語言的一元運(yùn)算符是編譯器的內(nèi)置能力不需要包含任何頭文件就能用。它計(jì)算的是類型或變量在內(nèi)存中占用的字節(jié)數(shù)編譯期間就出結(jié)果了。需要包含頭文件的場景是printf這類真正的庫函數(shù)你想在51上使用格式化輸出才需要包含stdio.h。但51上用printf要特別注意標(biāo)準(zhǔn)庫的printf默認(rèn)是往串口0輸出的你需要重映射底層字符發(fā)送函數(shù)比如改寫putchar或配置好串口否則printf編譯能過運(yùn)行時(shí)卻不會出結(jié)果。我早期就在這里卡過串口調(diào)數(shù)據(jù)printf打出來的永遠(yuǎn)是空串后來才反應(yīng)過來是stdout沒有重定向到UART。這種問題頭文件本身解決不了必須先理解它的機(jī)制再動手。這幾年幫人改過不少51工程我最深的體會是真正拖慢進(jìn)度的往往不是某個(gè)驅(qū)動難寫而是頭文件職責(zé)不清、聲明混亂導(dǎo)致的連鎖錯(cuò)誤。頭文件說白了就是工程的地基地基打得穩(wěn)后面加再多功能也都是往模塊里填代碼的事。把LED、按鍵、數(shù)碼管、LCD1602、串口、舵機(jī)這些常用模塊的頭文件沉淀下來形成自己的一套模板之后再接到電子時(shí)鐘、智能小車、溫控風(fēng)扇這類綜合項(xiàng)目基本就是把對應(yīng)的模塊像搭積木一樣拼起來。我個(gè)人始終建議選一條固定的、自己看得舒服的風(fēng)格堅(jiān)持下去時(shí)間會給你回報(bào)。本文還有配套的精品資源點(diǎn)擊獲取