这两年芯片的价格越炒越贵,特别是像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! "); return -1; } SMBus1_Send_Byte(Comcode); Ddl_Delay1us(90); //需要注意的是,这个地方的延时特别长 if(SMBus1_Wait_Ack() == 1) { batterry_info.LostContact[0] = 1; // printf("Comcode wait ack fail! "); 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! "); 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; }