這兩年芯片的價格越炒越貴,特別是像STM32,TI,NXP等等知名品牌更是水漲船高,甚至有時候你即使你願意花大價錢去購買,也不一定能買得到,所以很多公司紛紛轉向了國產芯片。國產芯片其實也不太好買得到,價錢也不便宜,而且可供參考的資料也是寥寥無幾,有時候你遇到了問題想上網找資料都很難找到可以參考的,這也是國產芯片的一個弱勢吧。
廢話不多說了,接下來談談華大單片機的一些應用吧。因為貨期和價格的問題,我所在公司的一些方案也開始轉向使用國產單片機,綜合來看,華大MCU是一個比較好的選擇。剛開始應用這個MCU的時候我遇到了不少問題,現在記錄下來,以防自己忘記了。
1、Timer0
華大MCU有提供庫函數,可以在KEIL或者IAR軟件上進行編程,大部分常用的外設都可以找到,像串口,定時器,I2C,SPI等等都可以找到demo code,可以直接上華大的官網查找。但是華大的庫函數提供的延時函數並不准確,延時1ms實際上延時了1.4ms左右。延時1us實際延時了2.4us。如下圖:
這個時間差距是有點離譜,連FAE也坦言一般情況下他們都不會用到這兩個延時,因為這兩個延時好像是利用內部RC振盪器分頻得到的,所以並不准確,且容易受到溫度的影響。
所以我動手寫了一個利用Timer0的精准延時,開始的設想是不利用中斷,因為利用中斷有些浪費資源了,但是實驗效果並不是特別好,延時不准確,所以我還是開啟了中斷,只是在沒有使用延時的時候,可以把時間設置得長一些,這樣可以避免頻繁進入中斷。假設要寫一個精准的微妙級別的延時函數,思路是這樣的,把Timer0的時鍾源選擇為PCLK1,然后通過分頻把頻率降到1MHz,這樣的話就可以CNT每隔1us就會增加一次,然后設置基准值寄存器(TMR0_CMPA<B>R
)的值,當CNT增加到基准值寄存器的數值時將會產生一個中斷。比如基准值寄存器設置為10,那就是10us會產生一個中斷,進入中斷后對某一個我們自定義的標志位進行置位,同時延時函數中等待這個標志位置位,等不到就死等,這樣就可以形成一個精准延時。具體代碼如下:
/* 定時器時鍾初始化 */ void TimerInit(void) { stc_tim0_base_init_t stcTimerCfg; stc_irq_regi_conf_t stcIrqRegiConf; stc_port_init_t stcPortInit; stc_clk_freq_t stcClkTmp; uint32_t u32tmp; MEM_ZERO_STRUCT(stcTimerCfg); MEM_ZERO_STRUCT(stcIrqRegiConf); MEM_ZERO_STRUCT(stcPortInit); /* Get pclk1 */ CLK_GetClockFreq(&stcClkTmp); u32Pclk1 = stcClkTmp.pclk1Freq; /* Enable XTAL32 */ CLK_Xtal32Cmd(Enable); /* Timer0 peripheral enable */ ENABLE_TMR0(); /*config register for channel B */ stcTimerCfg.Tim0_CounterMode = Tim0_Sync; stcTimerCfg.Tim0_SyncClockSource = Tim0_Pclk1; stcTimerCfg.Tim0_ClockDivision = Tim0_ClkDiv2; stcTimerCfg.Tim0_CmpValue = (uint16_t)((u32Pclk1/1000000/2ul)*1000 - 1ul); TIMER0_BaseInit(TMR_UNIT,Tim0_ChannelB,&stcTimerCfg); /* Enable channel B interrupt */ TIMER0_IntCmd(TMR_UNIT,Tim0_ChannelB,Enable); /* Register TMR_INI_GCMB Int to Vect.No.002 */ stcIrqRegiConf.enIRQn = Int002_IRQn; /* Select I2C Error or Event interrupt function */ stcIrqRegiConf.enIntSrc = TMR_INI_GCMB; /* Callback function */ stcIrqRegiConf.pfnCallback = &Timer0B_CallBack; /* Registration IRQ */ enIrqRegistration(&stcIrqRegiConf); /* Clear Pending */ NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn); /* Set priority */ NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIORITY_15); /* Enable NVIC */ NVIC_EnableIRQ(stcIrqRegiConf.enIRQn); /*start timer0*/ TIMER0_Cmd(TMR_UNIT,Tim0_ChannelB,Enable); }
//中斷回調函數 void Timer0B_CallBack(void) { /*同步計數方式中斷,該方式定時更加准確*/ TimerFlag = 1; }
//延時函數 void TIM0_CHB_Delay_us(uint16_t us) { TMR_UNIT->CMPBR = (u32Pclk1/1000000/2ul)*us-1ul; TMR_UNIT->CNTBR = 0; TimerFlag = 0; while(!TimerFlag); //延時時間到了,重新修改基准值寄存器的值,使其不頻繁進入中斷,不過不設置也是可以的 TMR_UNIT->CMPBR = (u32Pclk1/1000000/2ul)*1000-1ul; TMR_UNIT->CNTBR = 0; }
但是不知道什么原因,1us的時候並不准確,1us延時的時候實際測得是2.6us,但是1us以上就很精確了。
2、GPIO
華大MCU的GPIO有一些默認是具備特殊功能的,PA13,PA14,PA15,PB3,PB4 端口復位后初始狀態為 JTAG/SWD 功能有效,我的板子剛好LED燈是接在PA15上的,所以當我的板子在上電的時候,就看到這個LED亮着,但又不是完全亮着,用萬用表量了電平是1.7V左右,無論我怎么操作這個引腳,這個引腳的電平就是不為所動。后面才知道原因是因為它默認就是特殊功能,如果要正常操作這個引腳,必須修改它的功能,步驟就是:
①需要先解鎖,才能對寄存器進行修改;
②因為要把這個引腳的默認狀態TDI修改到GPO,所以需要先使這個TDI的功能無效,具體是修改特殊控制寄存器(PSRCR)b3的值,從1改為0;
③功能選擇寄存器(PFSRxy,x=A,B,...,H,y=1,2,...,15)的FSEL[5:0]設為b000000,表示選擇為Func0
④上鎖
具體代碼:
stc_port_init_t stcPortInit; MEM_ZERO_STRUCT(stcPortInit); stcPortInit.enPinMode = Pin_Mode_Out; stcPortInit.enExInt = Enable; stcPortInit.enPullUp = Disable; PORT_Unlock(); M4_PORT->PSPCR = 0x17; M4_PORT->PFSRA15 &= ~(0x3f); PORT_Lock(); //#define LED1_PORT (PortA) //#define LED1_PIN (Pin15) PORT_Init(LED1_PORT, LED1_PIN, &stcPortInit);
3、UART
在這次項目中,UART我是直接從官方例程中移植到我的項目中,但是發現並沒有數據傳送出來,或者隔了很久才接收到板子上發出的一些錯誤的數據。所以我用KEIL仿真模式進行調試,發現程序死在了 BEAB BKPT 0xAB處,上網查找了資料,具體的原因我還是不太清楚,大概就是我使用了printf()函數,使用了半主機模式,就會出現這種情況,解決的辦法就是使用微庫,也就是MiclroLIB,即勾選上USE MiclroLIB,重新編譯即可。如圖:
這個我在使用STM32單片機的時候沒有遇到過,好像只要重定向了都可以。
4、SMBus
SMBus是一種類似I2C的協議,大多數情況下工程師都會選擇用模擬SMBus來進行通訊,當然也可以用硬件SMBus。在上一版項目中我也是用了模擬SMBus來實現通訊的,經過驗證並沒有問題,這一次我在移植過來的時候發現通訊不上了,用示波器和邏輯分析儀看過,還是不知道問題出在哪里(邏輯分析儀用得不熟練)。我用示波器看過官方的延時函數的精度,發現差距比較大,所以懷疑是延時函數的問題,導致時序出錯,所以我才研究了用定時器0(Timer0)做精准延時的函數(上面有講述),但是發現實際上還是沒有作用。后面我去慢慢一條一條地比較代碼,終於發現一點蛛絲馬跡,原來我在采集電池信息的時候,沒有參考上一版的程序,當時覺得寫得比較亂,所以在網上找了一點資料參考,邏輯還是一樣的,只是在某些地方延時不一樣,網上的資料延時比較短,當我完完全全復制我前一版代碼的時候,發現問題解決了!我勒個天,我調了好幾天沒調出來的問題居然是不夠自信,沒有參考自己的勞動成果造成的!
接下來,我把SMBus的代碼貼出來,除了自己以后可以參考,也希望可以幫到有需要的人,這個代碼是MCU作為主機通過SMBus跟電池通訊(電池的電源控制芯片是BQ4050,默認從機地址是0x16):
#include "SMBus.h" #include "timer.h" /******************************** SMBus 1 *#define SMBus1_SCL_PORT (PortB) #define SMBus1_SCL_PIN (Pin06) #define SMBus1_SDA_PORT (PortB) #define SMBus1_SDA_PIN (Pin07) #define SMBus1_SCL_H PORT_SetBits(SMBus1_SCL_PORT,SMBus1_SCL_PIN) #define SMBus1_SCL_L PORT_ResetBits(SMBus1_SCL_PORT,SMBus1_SCL_PIN) #define SMBus1_SDA_H PORT_SetBits(SMBus1_SDA_PORT,SMBus1_SDA_PIN) #define SMBus1_SDA_L PORT_ResetBits(SMBus1_SDA_PORT,SMBus1_SDA_PIN) #define SMBus1_READ_SDA PORT_GetBit(SMBus1_SDA_PORT,SMBus1_SDA_PIN) * * ********************************/ /********************************* *函數名稱:void SMBus1_SDA_OUT(void) *函數功能:SDA線的引腳配置為輸出 *函數形參:無 *函數返回值:無 *********************************/ void SMBus1_SDA_OUT(void) { stc_port_init_t stcPortInit; MEM_ZERO_STRUCT(stcPortInit); stcPortInit.enPinMode = Pin_Mode_Out; stcPortInit.enExInt = Enable; stcPortInit.enPullUp = Enable; stcPortInit.enPinDrv = Pin_Drv_H; stcPortInit.enPinOType = Pin_OType_Cmos; PORT_Init(SMBus1_SDA_PORT, SMBus1_SDA_PIN, &stcPortInit); } /********************************* *函數名稱:void SMBus1_SDA_IN(void) *函數功能:SDA線的引腳配置為輸入 *函數形參:無 *函數返回值:無 *********************************/ void SMBus1_SDA_IN(void) { stc_port_init_t stcPortInit; MEM_ZERO_STRUCT(stcPortInit); stcPortInit.enPinMode = Pin_Mode_In; stcPortInit.enExInt = Enable; stcPortInit.enPullUp = Enable; PORT_Init(SMBus1_SDA_PORT, SMBus1_SDA_PIN, &stcPortInit); } /********************************* *函數名稱:void SMBus1_Init(void) *函數功能:SDA和SCL初始化 *函數形參:無 *函數返回值:無 *備注:都配置為上拉推挽輸出(不上拉,開漏好像也沒影響) *********************************/ void SMBus1_Init(void) { stc_port_init_t stcPortInit; MEM_ZERO_STRUCT(stcPortInit); stcPortInit.enPinMode = Pin_Mode_Out; stcPortInit.enExInt = Enable; stcPortInit.enPullUp = Enable; stcPortInit.enPinDrv = Pin_Drv_H; stcPortInit.enPinOType = Pin_OType_Cmos; PORT_Init(SMBus1_SCL_PORT, SMBus1_SCL_PIN, &stcPortInit); PORT_Init(SMBus1_SDA_PORT, SMBus1_SDA_PIN, &stcPortInit); SMBus1_SCL_H; SMBus1_SDA_H; } /********************************* *函數名稱:void SMBus1_Start(void) *函數功能:SMBus開始通訊 *函數形參:無 *函數返回值:無 *********************************/ void SMBus1_Start(void) { SMBus1_SDA_OUT(); //sda線輸出 SMBus1_SCL_L; Ddl_Delay1us(2); SMBus1_SDA_H; Ddl_Delay1us(1); SMBus1_SCL_H; Ddl_Delay1us(9); SMBus1_SDA_L; Ddl_Delay1us(9); SMBus1_SCL_L;//鉗住I2C總線,准備發送或接收數據 } /********************************* *函數名稱:void SMBus1_Stop(void) *函數功能:SMBus停止通訊 *函數形參:無 *函數返回值:無 *********************************/ void SMBus1_Stop(void) { SMBus1_SDA_OUT(); //sda線輸出 SMBus1_SCL_L; Ddl_Delay1us(1); SMBus1_SDA_L; //STOP:when CLK is high DATA change form low to high Ddl_Delay1us(9); SMBus1_SCL_H; Ddl_Delay1us(9); SMBus1_SDA_H;//發送I2C總線結束信號 Ddl_Delay1us(9); } /*********************************************** *函數名稱:uint8_t SMBus1_Wait_Ack(void) *函數功能:SMBus等待應答 *函數形參:無 *函數返回值:uint8_t類型,返回1表示超時,返回0表示接收到應答 ************************************************/ uint8_t SMBus1_Wait_Ack(void) { uint16_t uErrTime=0; SMBus1_SDA_IN(); //SDA設置為輸入 SMBus1_SDA_H; Ddl_Delay1us(9); SMBus1_SCL_H; Ddl_Delay1us(9); while(SMBus1_READ_SDA) { uErrTime++; if(uErrTime > 250) { SMBus1_Stop(); return 1; } //hrt_delay_us(1); } SMBus1_SCL_L; //時鍾輸出0 return 0; } /*********************************************** *函數名稱:void SMBus1_Ack(void) *函數功能:SMBus產生應答信號 *函數形參:無 *函數返回值:無 ************************************************/ void SMBus1_Ack(void) { SMBus1_SCL_L; SMBus1_SDA_OUT(); SMBus1_SDA_L; Ddl_Delay1us(9); SMBus1_SCL_H; Ddl_Delay1us(9); SMBus1_SCL_L; } /*********************************************** *函數名稱:void SMBus1_Ack(void) *函數功能:SMBus產生非應答信號 *函數形參:無 *函數返回值:無 ************************************************/ void SMBus1_NAck(void) { SMBus1_SCL_L; SMBus1_SDA_OUT(); SMBus1_SDA_H; Ddl_Delay1us(9); SMBus1_SCL_H; Ddl_Delay1us(9); SMBus1_SCL_L; } /*********************************************** *函數名稱:void SMBus1_Send_Byte(void) *函數功能:SMBus發送一個字節的數據 *函數形參:無 *函數返回值:無 ************************************************/ void SMBus1_Send_Byte(uint8_t txd) { uint8_t t=0; SMBus1_SDA_OUT(); SMBus1_SCL_L;//拉低時鍾開始數據傳輸 for(t=0;t<8;t++) { if((txd&0x80)>>7) { SMBus1_SDA_H; } else { SMBus1_SDA_L; } txd <<= 1; Ddl_Delay1us(8); SMBus1_SCL_H; Ddl_Delay1us(8); SMBus1_SCL_L; Ddl_Delay1us(8); } } /*********************************************** *函數名稱:uint8_t SMBus1_Read_Byte(void) *函數功能:SMBus接收一個字節的數據 *函數形參:無 *函數返回值:返回這個數據 ************************************************/ uint8_t SMBus1_Read_Byte(void) { uint8_t i; uint8_t recv=0; SMBus1_SDA_IN(); //SDA設置為輸入 for(i=0; i<8; i++) { SMBus1_SCL_L; Ddl_Delay1us(12); SMBus1_SCL_H; recv <<= 1; if(SMBus1_READ_SDA) { recv++; } Ddl_Delay1us(9); } return recv; }
通訊的基礎函數在網上都可以找得到,接下來是跟BQ4050的通訊部分,獲取電池信息:
/************************************************************ *函數名稱:int16_t Get_Battery1_Info(uint8_t slaveAddr, uint8_t Comcode) *函數功能:獲取電池信息 *函數形參:slaveAddr,從機地址,Comcode,命令 *函數返回值:將數據返回出來,可能是電壓,電流,RSOC,RMC,溫度等,具體跟Comcode相關 *************************************************************/ int16_t Get_Battery1_Info(uint8_t slaveAddr, uint8_t Comcode) { int16_t Value; uint8_t data[2] = {0}; SMBus1_Start(); SMBus1_Send_Byte(slaveAddr);//發送地址 if(SMBus1_Wait_Ack() == 1) { batterry_info.LostContact[0] = 1; // printf("SlaveAddr wait ack fail!\r\n"); return -1; } SMBus1_Send_Byte(Comcode); Ddl_Delay1us(90); //需要注意的是,這個地方的延時特別長 if(SMBus1_Wait_Ack() == 1) { batterry_info.LostContact[0] = 1; // printf("Comcode wait ack fail!\r\n"); return -1; } SMBus1_Start(); SMBus1_Send_Byte(slaveAddr|0x01);//發送地址 if(SMBus1_Wait_Ack() == 1) { batterry_info.LostContact[0] = 1; // printf("slaveAddr+1 wait ack fail!\r\n"); return -1; } Ddl_Delay1us(50); //需要注意的是,這個地方的延時特別長 data[0] = SMBus1_Read_Byte(); SMBus1_Ack(); Ddl_Delay1us(125); //需要注意的是,這個地方的延時特別長 data[1] = SMBus1_Read_Byte(); SMBus1_NAck(); Ddl_Delay1us(58); SMBus1_Stop(); Value = (data[0] |(data[1]<<8)); batterry_info.LostContact[0] = 0; Ddl_Delay1us(100); return Value; }