新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 第43节:通过串口用计数延时方式发送一串数据

第43节:通过串口用计数延时方式发送一串数据

作者: 时间:2016-11-22 来源:网络 收藏
开场白:
上一节讲了通过串口用delay延时方式发送一串数据,这种方式要求发送一串数据的时候一气呵成,期间不能执行其它任务,由于delay(400)这个时间还不算很长,所以可以应用在很多简单任务的系统中。但是在某些任务量很多的系统中,实时运行的主任务不允许被长时间和经常性地中断,这个时候就需要用计数延时来替代delay延时。本节要教会大家两个知识点:
第一个:用计数延时方式发送一串数据的程序框架。
第二个:环形消息队列的程序框架。

具体内容,请看源代码讲解。

(1)硬件平台:
基于朱兆祺51单片机学习板

(2)实现功能:
波特率是:9600.
用朱兆祺51单片机学习板中的S1,S5,S9,S13作为独立按键。
按一次按键S1,发送EB 00 55 01 00 00 00 00 41
按一次按键S5,发送EB 00 55 02 00 00 00 00 42
按一次按键S9,发送EB 00 55 03 00 00 00 00 43
按一次按键S13,发送EB 00 55 04 00 00 00 00 44
(3)源代码讲解如下:
  1. #include "REG52.H"
  2. #define const_send_time100//累计主循环次数的计数延时 请根据项目实际情况来调整此数据大小
  3. #define const_send_size10//串口发送数据的缓冲区数组大小
  4. #define const_Message_size10//环形消息队列的缓冲区数组大小
  5. #define const_key_time120 //按键去抖动延时的时间
  6. #define const_key_time220 //按键去抖动延时的时间
  7. #define const_key_time320 //按键去抖动延时的时间
  8. #define const_key_time420 //按键去抖动延时的时间
  9. #define const_voice_short40 //蜂鸣器短叫的持续时间
  10. void initial_myself(void);
  11. void initial_peripheral(void);
  12. //void delay_short(unsigned int uiDelayshort);
  13. void delay_long(unsigned int uiDelaylong);
  14. void eusart_send(unsigned char ucSendData);//发送一个字节,内部没有每个字节之间的延时
  15. void send_service(void);//利用累计主循环次数的计数延时方式来发送一串数据
  16. void T0_time(void);//定时中断函数
  17. void usart_receive(void); //串口接收中断函数
  18. void key_service(void); //按键服务的应用程序
  19. void key_scan(void); //按键扫描函数 放在定时中断里
  20. void insert_message(unsigned char ucMessageTemp);//插入新的消息到环形消息队列里
  21. unsigned char get_message(void);//从环形消息队列里提取消息
  22. sbit led_dr=P3^5;//Led的驱动IO口
  23. sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
  24. sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
  25. sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
  26. sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
  27. sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键
  28. sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
  29. unsigned char ucSendregBuf[const_send_size]; //串口发送数据的缓冲区数组
  30. unsigned char ucMessageBuf[const_Message_size]; //环形消息队列的缓冲区数据
  31. unsigned intuiMessageCurrent=0;//环形消息队列的取数据当前位置
  32. unsigned intuiMessageInsert=0;//环形消息队列的插入新消息时候的位置
  33. unsigned intuiMessageCnt=0;//统计环形消息队列的消息数量等于0时表示消息队列里没有消息
  34. unsigned char ucMessage=0; //当前获取到的消息
  35. unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
  36. unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁
  37. unsigned char ucKeySec=0; //被触发的按键编号
  38. unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
  39. unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
  40. unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
  41. unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
  42. unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
  43. unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志
  44. unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
  45. unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志
  46. unsigned char ucSendStep=0;//发送一串数据的运行步骤
  47. unsigned intuiSendTimeCnt=0; //累计主循环次数的计数延时器
  48. unsigned int uiSendCnt=0; //发送数据时的中间变量
  49. void main()
  50. {
  51. initial_myself();
  52. delay_long(100);
  53. initial_peripheral();
  54. while(1)
  55. {
  56. key_service(); //按键服务的应用程序
  57. send_service();//利用累计主循环次数的计数延时方式来发送一串数据
  58. }
  59. }
  60. /* 注释一:
  61. * 通过判断数组下标是否超范围的条件,把一个数组的首尾连接起来,就像一个环形,
  62. * 因此命名为环形消息队列。环形消息队列有插入消息,获取消息两个核心函数,以及一个
  63. * 统计消息总数的uiMessageCnt核心变量,通过此变量,我们可以知道消息队列里面是否有消息需要处理.
  64. * 我在做项目中很少用消息队列的,印象中我只在两个项目中用过消息队列这种方法。大部分的单片机
  65. * 项目其实直接用一两个中间变量就可以起到传递消息的作用,就能满足系统的要求。以下是各变量的含义:
  66. * #define const_Message_size10//环形消息队列的缓冲区数组大小
  67. * unsigned char ucMessageBuf[const_Message_size]; //环形消息队列的缓冲区数据
  68. * unsigned intuiMessageCurrent=0;//环形消息队列的取数据当前位置
  69. * unsigned intuiMessageInsert=0;//环形消息队列的插入新消息时候的位置
  70. * unsigned intuiMessageCnt=0;//统计环形消息队列的消息数量等于0时表示消息队列里没有消息
  71. */
  72. void insert_message(unsigned char ucMessageTemp)//插入新的消息到环形消息队列里
  73. {
  74. if(uiMessageCnt
  75. {
  76. ucMessageBuf[uiMessageInsert]=ucMessageTemp;
  77. uiMessageInsert++;//插入新消息时候的位置
  78. if(uiMessageInsert>=const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
  79. {
  80. uiMessageInsert=0;
  81. }
  82. uiMessageCnt++; //消息数量累加等于0时表示消息队列里没有消息
  83. }
  84. }
  85. unsigned char get_message(void)//从环形消息队列里提取消息
  86. {
  87. unsigned char ucMessageTemp=0;//返回的消息中间变量,默认为0
  88. if(uiMessageCnt>0)//只有消息数量大于0时才可以提取消息
  89. {
  90. ucMessageTemp=ucMessageBuf[uiMessageCurrent];
  91. uiMessageCurrent++;//环形消息队列的取数据当前位置
  92. if(uiMessageCurrent>=const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
  93. {
  94. uiMessageCurrent=0;
  95. }
  96. uiMessageCnt--; //每提取一次,消息数量就减一等于0时表示消息队列里没有消息
  97. }
  98. return ucMessageTemp;
  99. }
  100. void send_service(void)//利用累计主循环次数的计数延时方式来发送一串数据
  101. {
  102. switch(ucSendStep)//发送一串数据的运行步骤
  103. {
  104. case 0: //从环形消息队列里提取消息
  105. if(uiMessageCnt>0)//说明有消息需要处理
  106. {
  107. ucMessage=get_message();
  108. switch(ucMessage) //消息处理
  109. {
  110. case 1:
  111. ucSendregBuf[0]=0xeb; //把准备发送的数据放入发送缓冲区
  112. ucSendregBuf[1]=0x00;
  113. ucSendregBuf[2]=0x55;
  114. ucSendregBuf[3]=0x01; //01代表1号键
  115. ucSendregBuf[4]=0x00;
  116. ucSendregBuf[5]=0x00;
  117. ucSendregBuf[6]=0x00;
  118. ucSendregBuf[7]=0x00;
  119. ucSendregBuf[8]=0x41;
  120. uiSendCnt=0; //发送数据的中间变量清零
  121. uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
  122. ucSendStep=1; //切换到下一步发送一串数据
  123. break;
  124. case 2:
  125. ucSendregBuf[0]=0xeb; //把准备发送的数据放入发送缓冲区
  126. ucSendregBuf[1]=0x00;
  127. ucSendregBuf[2]=0x55;
  128. ucSendregBuf[3]=0x02; //02代表2号键
  129. ucSendregBuf[4]=0x00;
  130. ucSendregBuf[5]=0x00;
  131. ucSendregBuf[6]=0x00;
  132. ucSendregBuf[7]=0x00;
  133. ucSendregBuf[8]=0x42;
  134. uiSendCnt=0; //发送数据的中间变量清零
  135. uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
  136. ucSendStep=1; //切换到下一步发送一串数据
  137. break;
  138. case 3:
  139. ucSendregBuf[0]=0xeb; //把准备发送的数据放入发送缓冲区
  140. ucSendregBuf[1]=0x00;
  141. ucSendregBuf[2]=0x55;
  142. ucSendregBuf[3]=0x03; //03代表3号键
  143. ucSendregBuf[4]=0x00;
  144. ucSendregBuf[5]=0x00;
  145. ucSendregBuf[6]=0x00;
  146. ucSendregBuf[7]=0x00;
  147. ucSendregBuf[8]=0x43;
  148. uiSendCnt=0; //发送数据的中间变量清零
  149. uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
  150. ucSendStep=1; //切换到下一步发送一串数据
  151. break;
  152. case 4:
  153. ucSendregBuf[0]=0xeb; //把准备发送的数据放入发送缓冲区
  154. ucSendregBuf[1]=0x00;
  155. ucSendregBuf[2]=0x55;
  156. ucSendregBuf[3]=0x04; //04代表4号键
  157. ucSendregBuf[4]=0x00;
  158. ucSendregBuf[5]=0x00;
  159. ucSendregBuf[6]=0x00;
  160. ucSendregBuf[7]=0x00;
  161. ucSendregBuf[8]=0x44;
  162. uiSendCnt=0; //发送数据的中间变量清零
  163. uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
  164. ucSendStep=1; //切换到下一步发送一串数据
  165. break;
  166. default://如果没有符合要求的消息,则不处理
  167. ucSendStep=0; //维持现状,不切换
  168. break;
  169. }
  170. }
  171. break;
  172. case 1://利用累加主循环次数的计数延时方式来发送一串数据
  173. /* 注释二:
  174. * 这里的计数延时为什么不用累计定时中断次数的延时,而用累计主循环次数的计数延时?
  175. * 因为本程序定时器中断一次需要500个指令时间,时间分辨率太低,不方便微调时间。因此我
  176. * 就用累计主循环次数的计数延时方式,在做项目的时候,各位读者应该根据系统的实际情况
  177. * 来调整const_send_time的大小。
  178. */
  179. uiSendTimeCnt++;//累计主循环次数的计数延时,为每个字节之间增加延时,
  180. if(uiSendTimeCnt>const_send_time)//请根据实际系统的情况,调整const_send_time的大小
  181. {
  182. uiSendTimeCnt=0;
  183. eusart_send(ucSendregBuf[uiSendCnt]);//发送一串数据给上位机
  184. uiSendCnt++;
  185. if(uiSendCnt>=9) //说明数据已经发送完毕
  186. {
  187. uiSendCnt=0;
  188. ucSendStep=0; //返回到上一步,处理其它未处理的消息
  189. }
  190. }
  191. break;
  192. }
  193. }
  194. void eusart_send(unsigned char ucSendData)
  195. {
  196. ES = 0; //关串口中断
  197. TI = 0; //清零串口发送完成中断请求标志
  198. SBUF =ucSendData; //发送一个字节
  199. /* 注释三:
  200. * 根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。
  201. * 当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前
  202. * 在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),
  203. * 所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。
  204. * 但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。
  205. */
  206. //delay_short(400);//因为外部在每个发送字节之间用了累计主循环次数的计数延时,因此不要此行的delay延时
  207. TI = 0; //清零串口发送完成中断请求标志
  208. ES = 1; //允许串口中断
  209. }
  210. void key_scan(void)//按键扫描函数 放在定时中断里
  211. {
  212. if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
  213. {
  214. ucKeyLock1=0; //按键自锁标志清零
  215. uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
  216. }
  217. else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
  218. {
  219. uiKeyTimeCnt1++; //累加定时中断次数
  220. if(uiKeyTimeCnt1>const_key_time1)
  221. {
  222. uiKeyTimeCnt1=0;
  223. ucKeyLock1=1;//自锁按键置位,避免一直触发
  224. ucKeySec=1; //触发1号键
  225. }
  226. }
  227. if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
  228. {
  229. ucKeyLock2=0; //按键自锁标志清零
  230. uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
  231. }
  232. else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
  233. {
  234. uiKeyTimeCnt2++; //累加定时中断次数
  235. if(uiKeyTimeCnt2>const_key_time2)
  236. {
  237. uiKeyTimeCnt2=0;
  238. ucKeyLock2=1;//自锁按键置位,避免一直触发
  239. ucKeySec=2; //触发2号键
  240. }
  241. }
  242. if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
  243. {
  244. ucKeyLock3=0; //按键自锁标志清零
  245. uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
  246. }
  247. else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
  248. {
  249. uiKeyTimeCnt3++; //累加定时中断次数
  250. if(uiKeyTimeCnt3>const_key_time3)
  251. {
  252. uiKeyTimeCnt3=0;
  253. ucKeyLock3=1;//自锁按键置位,避免一直触发
  254. ucKeySec=3; //触发3号键
  255. }
  256. }
  257. if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
  258. {
  259. ucKeyLock4=0; //按键自锁标志清零
  260. uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
  261. }
  262. else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
  263. {
  264. uiKeyTimeCnt4++; //累加定时中断次数
  265. if(uiKeyTimeCnt4>const_key_time4)
  266. {
  267. uiKeyTimeCnt4=0;
  268. ucKeyLock4=1;//自锁按键置位,避免一直触发
  269. ucKeySec=4; //触发4号键
  270. }
  271. }
  272. }
  273. void key_service(void) //第三区 按键服务的应用程序
  274. {
  275. switch(ucKeySec) //按键服务状态切换
  276. {
  277. case 1:// 1号键 对应朱兆祺学习板的S1键
  278. insert_message(0x01);//把新消息插入到环形消息队列里等待处理
  279. ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
  280. uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
  281. ucVoiceLock=0; //原子锁解锁
  282. ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
  283. break;
  284. case 2:// 2号键 对应朱兆祺学习板的S5键
  285. insert_message(0x02);//把新消息插入到环形消息队列里等待处理
  286. ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
  287. uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
  288. ucVoiceLock=0; //原子锁解锁
  289. ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
  290. break;
  291. case 3:// 3号键 对应朱兆祺学习板的S9键
  292. insert_message(0x03);//把新消息插入到环形消息队列里等待处理
  293. ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
  294. uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
  295. ucVoiceLock=0; //原子锁解锁
  296. ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
  297. break;
  298. case 4:// 4号键 对应朱兆祺学习板的S13键
  299. insert_message(0x04);//把新消息插入到环形消息队列里等待处理
  300. ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
  301. uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
  302. ucVoiceLock=0; //原子锁解锁
  303. ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
  304. break;
  305. }
  306. }
  307. void T0_time(void) interrupt 1 //定时中断
  308. {
  309. TF0=0;//清除中断标志
  310. TR0=0; //关中断
  311. /* 注释四:
  312. * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  313. */
  314. if(ucVoiceLock==0) //原子锁判断
  315. {
  316. if(uiVoiceCnt!=0)
  317. {
  318. uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
  319. beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
  320. }
  321. else
  322. {
  323. ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
  324. beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
  325. }
  326. }
  327. key_scan();//按键扫描函数
  328. TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
  329. TL0=0x0b;
  330. TR0=1;//开中断
  331. }
  332. void usart_receive(void) interrupt 4 //串口中断
  333. {
  334. if(RI==1)
  335. {
  336. RI = 0; //接收中断,及时把接收中断标志位清零
  337. }
  338. else
  339. {
  340. TI = 0; //发送中断,及时把发送中断标志位清零
  341. }
  342. }
  343. //void delay_short(unsigned int uiDelayShort)
  344. //{
  345. // unsigned int i;
  346. // for(i=0;i
  347. // {
  348. // ; //一个分号相当于执行一条空语句
  349. // }
  350. //}
  351. void delay_long(unsigned int uiDelayLong)
  352. {
  353. unsigned int i;
  354. unsigned int j;
  355. for(i=0;i
  356. {
  357. for(j=0;j<500;j++)//内嵌循环的空指令数量
  358. {
  359. ; //一个分号相当于执行一条空语句
  360. }
  361. }
  362. }
  363. void initial_myself(void)//第一区 初始化单片机
  364. {
  365. /* 注释五:
  366. * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
  367. * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
  368. * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
  369. */
  370. key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
  371. led_dr=0; //关Led灯
  372. beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
  373. //配置定时器
  374. TMOD=0x01;//设置定时器0为工作方式1
  375. TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
  376. TL0=0x0b;
  377. //配置串口
  378. SCON=0x50;
  379. TMOD=0X21;
  380. TH1=TL1=-(11059200L/12/32/9600);//串口波特率9600。
  381. TR1=1;
  382. }
  383. void initial_peripheral(void) //第二区 初始化外围
  384. {
  385. EA=1; //开总中断
  386. ES=1; //允许串口中断
  387. ET0=1; //允许定时中断
  388. TR0=1; //启动定时中断
  389. }

总结陈词:
前面几个章节中,每个章节要么独立地讲解串口收数据,要么独立地讲解发数据,实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。读者只要把我的串口收发程序结合起来,就很容易实现这样的功能,我就不再详细讲解了。从下一节开始我讲解单片机掉电后数据保存的内容,欲知详情,请听下回分解-----利用AT24C02进行掉电后的数据保存。


评论


技术专区

关闭