起因
用 STC8H 做产品经常需要两路串口:一路做调试口,另一路接模块(WiFi/蓝牙/485)。串口1 通常被调试口占着,串口2 就是为第二路准备的。本文演示 STC8H 串口2 的初始化、发送、中断接收的最小程序。
注意:要和外部设备通信,stc单片机最好用外部晶振,内部IRC时钟波特率不准,无法正常通信。原因见下面原理一节。
需求
- 串口2 以 9600bps 收发(11.0592MHz 时钟)
- 中断方式接收,主循环里处理数据
技术实现原理
串口2 和串口1 的本质区别:各自的波特率发生器
串口1 经典用法拿定时器1 当波特率发生器;串口2 固定用定时器2,两路互不抢占——这就是“第二路串口几乎零成本”的原因:不用轮换定时器,两路同时工作。寄存器也是独立一套:S2CON(控制)、S2BUF(数据)、IE2 位 0(中断使能)、中断号 8。
波特率初值是怎么算出来的(9600@11.0592MHz)
STC8 的串口在 1T 模式下:波特率 = Fosc / 4 / (65536 − 重装值)。反过来算:重装值 = 65536 − 11059200/4/9600 = 65536 − 288 = 65248 = 0xFEE0。对照代码:T2H=0xFE、T2L=0xE0,正是这个值。AUXR |= 0x04 把定时器2 设成 1T(时钟不分频),AUXR |= 0x10 启动它。
为什么要强调晶振/IRC 的选择
UART 是“按约定节拍逐位采样”的协议,没有时钟线、没有纠错——双方波特率误差积累到停止位时若超过半个位宽,采样就错,经验值是误差 2%~3% 以内才安全。内部 IRC 有出厂偏差(±0.3% 量级)且随温漂;而 11.0592MHz 能被常用波特率整除(9600×288×4 = 11059200),理论误差为 0。这就是“要和外部设备通信最好用外部晶振;用 IRC 就在下载软件里选 11.0592MHz”的底层原因。
接线是交叉的
P1.0 是 RxD2(输入,配准双向口)、P1.1 是 TxD2(输出,配推挽增加驱动)。P10→UTXD、P11→URXD 是交叉接法:USB 转串口模块的 UTXD 是它的发送脚,要接单片机的接收脚——谁的 TX 接谁的 RX,别同名对同名。
接线说明:
STC8A单片机-->USB转TTL&电源模块
P10-->UTXD
P11-->URXD
注意事项: USB连接线给开发板通电下载程序,STC下载软件中内部IRC时钟选择11.0592MHZ
程序源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
|
/**************************************************************************************
接线说明: 1:STC8A单片机-->USB转TTL&电源模块
P10-->UTXD
P11-->URXD
注意事项: USB连接线给开发板通电下载程序,STC下载软件中内部IRC时钟选择11.0592MHZ
***************************************************************************************/
#include "STC8.H"
#include "intrins.h"
#define MAIN_Fosc 11059200L //定义主时钟
#define uint32 unsigned long
#define uint16 unsigned int
#define uint8 unsigned char
#define uchar unsigned char
/************* 本地变量声明 **************/
#define TRUE 1
#define FALSE 0
#define Buf_Max 50
#define S2_S 0x01
#define S2RI 0x01
#define S2TI 0x02
volatile uint8 Flag=FALSE;
uint8 xdata Rec_Buf[Buf_Max];
uint8 i = 0;
bit busy;
uchar table2[3]={0x04,0x04,0x04}; //串口2循环发送指令
uchar rec2[14]; //接收数组
uchar rec1[14];
/**************************************
功能描述:延时函数
入口参数:uint16 x ,该值为1时,延时1ms
返回值:无
***************************************/
void delay_ms(uint16 x)
{
uint16 j,i;
for(j=0;j<x;j++)
{
for(i=0;i<1100;i++);
}
}
/***************************************************************************
* 描 述 : 串口2初始化函数
* 入 参 : 无
* 返回值 : 无
备注:波特率9600bps 晶振11.0592MHz
**************************************************************************/
void Uart2_Init(void)
{
S2CON = 0x50; //8位数据,可变波特率,启动串行接收器
AUXR |= 0x04; //定时器2时钟为Fosc,即1T
T2L = 0xE0; //设定定时初值
T2H = 0xFE; //设定定时初值
AUXR |= 0x10; //启动定时器2
IE2 |= 0x01; // 串口2中断打开
EA = 1; // 总中断打开
}
/***************************************************************************
* 描 述 : 串口1初始化函数
* 入 参 : 无
* 返回值 : 无
备注:波特率9600bps 晶振11.0592MHz
**************************************************************************/
void Uart1_Init(void)
{
PCON &= 0x3f; //波特率不倍速,串行口工作方式由SM0、SM1决定
SCON = 0x50; //8位数据,可变波特率,启动串行接收器
AUXR |= 0x40; //定时器1时钟为Fosc,即1T
AUXR &= 0xfe; //串口1选择定时器1为波特率发生器
TMOD &= 0x0f; //清除定时器1模式位
TMOD |= 0x20; //设定定时器1为8位自动重装方式
TL1 = 0xDC; //设定定时初值
TH1 = 0xDC; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
ES = 1; // 串口1中断打开
EA = 1; // 总中断打开
}
/***************************************************************************
* 描 述 : 串口2发送数据函数
* 入 参 : uint8 数据
* 返回值 : 无
**************************************************************************/
void SendDataByUart2()
{
S2BUF =table2[3] ; //写数据到UART数据寄存器
while(!(S2CON&S2TI)); //在停止位没有发送时,S2TI为0即一直等待
S2CON&=~S2TI; //清除S2CON寄存器对应S2TI位(该位必须软件清零)
}
/**************************************************************************************
* 描 述 : USART2接收到数据后串口发送出去
* 入 参 : 无
* 返回值 : 无
**************************************************************************************/
void USART2_Tx_Puts(void)
{
if(Flag) //有新数据通过串口被接收到
{
IE2 &= 0xFE; // 串口2中断关闭
rec1[14]=rec2[14];
IE2 |= 0x01; // 串口2中断打开
Flag=FALSE; //清除接收标识符
}
}
/***************************************************************************
* 描 述 : 串口1发送数据函数
* 入 参 : uint8 数据
* 返回值 : 无
**************************************************************************/
void SendDataByUart1(void)
{
rec1[14]=S2BUF ; //写数据到UART数据寄存器
while(TI == 0); //在停止位没有发送时,TI为0即一直等待
TI = 0; //清除TI位(该位必须软件清零)
}
/**************************************************************************************
* 描 述 : USART1接收到数据后串口发送出去
* 入 参 : 无
* 返回值 : 无
**************************************************************************************/
void USART1_Tx_Puts(void)
{
if(Flag) //有新数据通过串口被接收到
{
ES = 0; //串口1中断关闭
SendDataByUart1(); //发送字符
ES = 1; //串口1中断打开
Flag=FALSE; //清除接收标识符
}
}
/***************************************************************************
* 描 述 : 串口2中断服务函数
* 入 参 : 无
* 返回值 : 无
**************************************************************************/
void Uart2() interrupt 8 using 1
{
IE2 &= 0xFE; // 串口2中断关闭
Flag=TRUE; //接收到数据,接收标识符有效
if (S2CON & S2RI) //串行接收到停止位的中间时刻时,该位置1
{
S2CON &= ~S2RI; //清除S2CON寄存器对应S2RI位(该位必须软件清零)
rec2[14] = S2BUF;
}
if (S2CON & S2TI) //在停止位开始发送时,该位置1
{
S2CON &= ~S2TI; //清除S2CON寄存器对应S2TI位(该位必须软件清零)
}
IE2 |= 0x01; // 串口2中断打开
}
/***********************
功能描述:主函数
入口参数:无
返回值:无
************************/
int main()
{
P1M1 &= 0xFE; P1M0 &= 0xFE; //设置P1.0为准双向口
P1M1 &= 0xFD; P1M0 |= 0x02; //设置P1.1为推挽输出
P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口
P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出
Uart2_Init();
Uart1_Init();
while(1);
}
///***************************************************************************
// * 描 述 : 串口1中断服务函数
// * 入 参 : 无
// * 返回值 : 无
// **************************************************************************/
//void Uart1() interrupt 4 using 1
//{
// ES = 0; // 串口1中断关闭
// Flag=TRUE; //接收到数据,接收标识符有效
// if (RI) //串行接收到停止位的中间时刻时,该位置1
// {
// RI = 0; //清除RI位 (该位必须软件清零)
// SBUF=S2BUF;
// }
// if (TI) //在停止位开始发送时,该位置1
// {
// TI = 0; //清除TI位(该位必须软件清零)
// }
// ES = 1; // 串口1中断打开
//}
|
代码说明
- Uart2_Init:S2CON=0x50(8 位可变波特率 + 允许接收);T2 初值 0xFEE0 对应 9600bps;IE2 位 0 开串口2 中断;
- Uart2() interrupt 8:串口2 的中断号是 8(不是串口1 的 4)。S2RI/S2TI 都在 S2CON 里,且必须软件清零;
- SendDataByUart2:写 S2BUF 后死等 S2TI 置位再清零,阻塞发送的最简写法;
- 接收路径:中断里读走 S2BUF + 置 Flag,主循环再处理——中断里只做最少的事,处理放主循环,这是裸机串口的标准分层。
踩坑记录与注意事项
- 示例代码里数组越界:
table2[3]、rec2[14]、rec1[14] 都是越界访问(数组长度 3 和 14,合法下标只到 2 和 13),rec1[14]=rec2[14] 也只拷了一个元素。这份代码当寄存器配置的骨架看,收发缓冲要按真实下标重写;
- S2RI/S2TI 必须软件清零:硬件不会自动清,忘了清会反复进中断,现象是“程序卡在中断里出不来”;
- main 里 while(1) 空转:示例没调用 USART2_Tx_Puts 等处理函数,实际使用要把处理逻辑挂进主循环;
- 1T/12T 别混:AUXR 的定时器2 时钟位决定初值公式差 12 倍,从老 51 例程(12T)抄初值过来波特率差一个数量级;
- IRC 用 11.0592MHz:在 STC 下载软件里把 IRC 频率选成 11.0592MHz(原理见上文),或外挂晶振,否则和外部设备通信出现规律性乱码/丢字节。