這兩年芯片的價格越炒越貴,特別是像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;
}
