转载注明出处。
沁恒MCU串口使用指南:
适用于WCH的32位MCU和CH559/558单片机
只描述TTL电平的TX+RX形式的常规串口,流控,RS232、RS485不在文章涉及范围之内。
大部分8位机按照标准的51单片机串口操作,建议自行搜索。
分为两部分,串口接收和串口发送:
一、串口发送:
首先摘抄CH573手册描述(请先仔细阅读一下,说不定就看明白了)
发送的时候我们肯定希望发送速率越快越好,我们就要充分利用“发送空中断”。因为CPU速度肯定比串口发送速度快,所以总是会存在CPU等待串口发送完成这一情况。
打个比方,有一辆可以坐8个乘客的小巴士,从公司带人去机场,怎么样最快呢,肯定是每次都坐满发车最快~。那怎么知道司机从机场回来,可以安排下一辆车的人了呢?两种方式:
1、司机给负责人打电话,喊人下来
2、负责人蹲公司门口看司机回来了没有,当然不一定要持续盯着,可以每隔几秒抬头看一下
两种通知方式,相比下肯定是第一种省力一点的,所以对应到串口上:
1、FIFO功能毫无疑问要打开(换成8座小巴士)
2、每次坐满发车(CPU连续填充8个字节)
3、通知方式可选中断(打电话通知)或者查询(蹲门口盯着),这个取决于对于串口连续发送的效率要求,中断方式效率会高,但是会增加系统复杂程度(中断优先级、嵌套等问题),查询效率没那么高,但是系统会简洁一点,同时查一下中断标志的开销不大(抬头看一眼,不怎么影响低头刷手机哈哈哈)
4、通知车回来之后,再安排8个人一起上车,如此循环
代码上上述功能的具体实现(伪代码):
1、
R8_UART1_FCR |= (1<<1);
2、
void UART1_SendString( UINT8 *buf )
{
UINT8 i;
for(i=0;i<8;i++){
R8_UART1_THR = *buf++;
}
}
3+4、
查询
void UART1_SendString( PUINT8 buf )
{
UINT8 i;
for(i=0;i<8;i++){
R8_UART1_THR = *buf++;
}
}
void main(){
R8_UART1_IER |= (1<<1); //使能串口空中断
while(1){
{
//cpu干别的事
}
if(R8_UART1_IIR & (1<<1)) //THR寄存器空
UART1_SendString( buf );
}
}
中断
void main(){
R8_UART1_IER |= (1<<1); //使能串口空中断
PFIC_EnableIRQ( UART1_IRQn ); //打开串口的中断功能
while( 1 );
}
__attribute__((interrupt("WCH-Interrupt-fast")))
__attribute__((section(".highcode")))
void UART1_IRQHandler( void )
{
if(R8_UART1_IIR & (1<<1)) //THR寄存器空
UART1_SendString( buf );
}
二、串口接收
串口的接收整体会麻烦很多,因为是被动的。先摘抄手册描述:(同样先仔细阅读一遍)
串口接收首先是个被动的过程,所以我们尽量的用中断来处理。先看一下和接收相关的中断类型:
①接收数据可用:接受的字节数达到FIFO的触发点
②接收数据超时:超过4个字节时间未收到下一数据
这两个中断足以应付绝大部分的场景了。因为串口有8字节的FIFO,进来的数据可以稍微缓冲一下,不至于一有数据就要处理,这会变得挺麻烦的,这点相比标准51单片机来说进步很多。
①接收数据可用中断:
因为数据有可能连续不断的一直发送过来,我们要及时的读取,但是又不能太及时,不然可能影响CPU处理别的东西。
打个比方,有个消毒房间,会从天花板上喷消毒水对工作人员进行消毒,这个消毒房间最多只能站8个人,每隔一分钟都会有人要进入消毒房间,单人单次消毒不到1分钟就可以完成,但是每多一个人多喷洒一小会的消毒水,防止出现死角没有喷洒到药水。所以我们有两种模式进行消毒:
①每来一个人就进去消毒,下一个人来的时候前一个人已经消毒结束了
②等多几个人,然后几个人一起进去消毒,下一个人来的时候几个人一起消毒结束了
两种方式显然都是可以的,但是会有以下的问题,第一种模式会浪费掉更多的药水,每个人都是100%的药水喷洒量,第二种模式平均到每个人的药水用量会节省不少。但是模式二如果进入人数太多,可能消毒所需要的时间会超过1分钟,导致下一个来的人被堵在门口了。所以第二种模式需要根据规则限制人数,防止后来的人堵在门口。
对应到串口接收上,串口每接收到一个数据就去处理当然可以,但是会导致CPU开销变大。启用“接收数据可用”中断能够节约CPU开销,但是可能存在CPU还没有读取FIFO中数据,下一个数据就到来的情况。所以可能当FIFO还没有满的时候,CPU就可以去将数据取出来,防止出现阻塞(对于串口来说实际上就是丢数据了),这时候产生“接收数据可用”中断的产生条件“FIFO触发点”就有价值了:
结合串口波特率、CPU频率配置合适的触发点,设置成4字节会比较保险且高效一点。
伪代码如下:
UINT8 flag=0;
UINT8 buf[4];
void main( ){
R8_UART1_FCR = (R8_UART1_FCR & ~(3<<6))) | (2<<6); //设置4字节FIFO触发点
R8_UART1_IER |= 1; //使能接收数据可用中断
while(1){
if(flag){
flag = 0;
printf("1:%02x\n",buf[0]);
printf("2:%02x\n",buf[1]);
printf("3:%02x\n",buf[2]);
printf("4:%02x\n",buf[3]);
}
}
}
__attribute__((interrupt("WCH-Interrupt-fast")))
__attribute__((section(".highcode")))
void UART1_IRQHandler( void )
{
UINT8 i;
if(R8_UART1_IIR & (1<<2)){ //中断源为数据接受可用
for(i=0;i<4;i++){
buf[i] = R8_UART1_RBR; //直接按照触发点长度读取若干次RBR寄存器
}
flag = 1;
}
}
②接收数据超时中断:
简单说就是串口空闲了会产生中断,主观上可以认为是一帧结束,配合“数据接收可用”中断。通常数据都是有格式,会有固定长度。假设我们FIFO触发点设置4,数据一帧是14字节,一分钟来一帧数据。我们也有两种方式去接收数据:
1、只判断“数据接收可用“中断,那么这一帧数据会触发3次中断,成功接收到12字节数据,最后剩下的2字节数据会保存在FIFO中,因为2字节不到触发点,所以CPU并不知道要去取数据。直到1分钟之后下一帧数据到来,新一帧的前2字节和前一帧的2字节凑成4字节,新一帧剩余的12字节正好能凑4的整数倍。这样数据倒是不会丢,但是出现了时效性的问题。
2、开启“接收数据超时“中断,处理前面的那种情况,第一帧到来之后,先连续触发3次FIFO触发点,CPU取走12字节数据,剩余2字节在FIFO中储存,串口空闲4字节时间(当前波特率连续发送4字节所需要的时间),产生了超时中断,因为知道一帧数据就是14字节,当产生超时中断时,FIFO中必然有2字节数据,此时直接读取两次RBR寄存器就可以将一整帧接收完成,而不用傻傻等一分钟那么久了。伪代码如下:
UINT8 flag1=0;
UINT8 flag2=0;
UINT8 buf[4];
void main( ){
R8_UART1_FCR = (R8_UART1_FCR & ~(3<<6))) | (2<<6); //设置4字节FIFO触发点
R8_UART1_IER |= 1; //使能接收数据可用中断
while(1){
if(flag1){
flag1 = 0;
printf("1:%02x\n",buf[0]);
printf("2:%02x\n",buf[1]);
printf("3:%02x\n",buf[2]);
printf("4:%02x\n",buf[3]);
}
if(flag2){
flag2 = 0;
printf("1:%02x\n",buf[0]);
printf("2:%02x\n",buf[1]);
}
}
}
__attribute__((interrupt("WCH-Interrupt-fast")))
__attribute__((section(".highcode")))
void UART1_IRQHandler( void )
{
UINT8 i;
switch(R8_UART1_IIR & (3<<2)){
case 0x04; //中断源为数据接受可用
for(i=0;i<4;i++){
buf[i] = R8_UART1_RBR; //直接按照触发点长度读取若干次RBR寄存器
}
flag1 = 1;
break;
case 0x0c: //中断源为超时
for(i=0;i<2;i++){
buf[i] = R8_UART1_RBR; //直接按照触发点长度读取若干次RBR寄存器
}
flag2 = 1;
break;
}
if(R8_UART1_IIR & (3<<2))
}
总结:
至此一个常规的16C550类串口就能够正常工作起来了,关于FIFO触发点的配置,超时中断的利用,还是要结合串口数据一帧的规律来调整。都看到这里了,其实手册讲的也很挺直白了、、、
`