作者:Hcamael****@知道创宇404实验室********
时间:2024年3月13日
从本篇开始就要研究USB设备开发硬件部分的知识,本系列硬件部分文章的学习案例来源于《圈圈教你玩USB》。
1 准备工作
参考资料
在硬件开发中,首要步骤是进行硬件设计,其中包括电路图设计、布线、出板和焊接等环节。
然而,针对新手而言,建议不要从最初的硬件设计阶段开始学习。相反,建议首先购买成品设备,然后掌握软件开发的知识,之后再尝试进行硬件设计的工作。
因此,本文将从单片机开发开始学习,后续文章将涵盖硬件设计的内容。在此前提下,第一步是需要在网上购买相关的开发板,可以在淘宝、咸鱼等网站上搜索关键字:"圈圈教你usb开发板"。在本系列中不提供购买链接,请自行解决开发板的问题。如果不想购买现成的开发板,并且对自己的动手能力有信心,可以参考后续文章进行硬件设计。
本文案例中所使用的设备如下图所示,总花费为57¥(个人自行设计打板的总花费并不比购买成品低)。
图1:USB开发51单片机
在购买了成品单片机后,还可以从商家获取该单片机的原理图,如下图所示:
图2:USB开发51单片机原理图
还需要了解两个主要芯片的型号,以便搜索相关文档,其中51单片机芯片的型号为:STC89C52RC
,USB芯片的型号为:PDIUSBD12
,知晓芯片型号后,可以通过搜索引擎获取相关文档,并在后续开发过程中参考这些文档。
对于没有进行过单片机开发的人来说,可以将单片机理解为集成了CPU、RAM和ROM的芯片。在后续开发工作中,我们控制单片机运行,编译出的程序需要写入(通常称为下载)到单片机的ROM中。不同的单片机具有不同的下载方式。对于STC89C52RC
单片机而言,可以通过TTL串口直接下载程序到单片机中。
因此,还需要准备一个串口线,由于开发板设计了RS-232串口母口,所以可以准备一个RS-232串口公口转USB线。或者直接用单片机的TTL串口,但这就需要准备一个TTL串口转USB的设备。
大部分情况下,开发单片机用的都是Windows系统,所以绝大部分好用的工具都是Windows程序。但是,我还是喜欢在Mac系统下做开发工作,经过研究,搭建了Mac下的单片机开发环境。
首先安装VSCode,再安装PlatformIO IDE
插件,这样一个轻量级的单片机开发环境就搭建完成了。
在本文的样例中,需要修改开发目录下的platformio.ini
,按以下示例进行修改:
; PlatformIO Project Configuration File;; Build options: build flags, source filter; Upload options: custom upload port, speed and extra flags; Library options: dependencies, extra library storages; Advanced options: extra scripting;; Please visit documentation for the other options and examples; https://docs.platformio.org/page/projectconf.html[env:STC89C52RC]platform = intel_mcs51board = STC89C52RCupload_port = /dev/tty.usbserial-0001 # ttl串口
按照上述过程进行环境搭建,不需要再额外的安装编译器,PlatformIO IDE
会自带编译器,使用的编译工具叫sdcc。另外,下载器(用于将编译好的程序写入单片机)使用的工具是stcgal,PlatformIO IDE
也可以将其一起安装好。但是有时候需要单独使用stcgal,如果在终端中直接使用PlatformIO IDE
中安装的stcgal会比较麻烦,步骤如下:
$ source ~/.platformio/penv/bin/activatepython3 ~/.platformio/packages/tool-stcgal/stcgal.py -husage: stcgal.py [-h] [-a] [-r RESETCMD] [-P {stc89,stc12a,stc12b,stc12,stc15a,stc15,stc8,usb15,auto}] [-p PORT] [-b BAUD] [-l HANDSHAKE] [-o OPTION] [-t TRIM] [-D] [-V] [code_image] [eeprom_image]stcgal 1.6 - an STC MCU ISP flash tool(C) 2014-2018 Grigori Goronzy and othershttps://github.com/grigorig/stcgalpositional arguments: code_image code segment file to flash (BIN/HEX) eeprom_image eeprom segment file to flash (BIN/HEX)options: -h, --help show this help message and exit -a, --autoreset cycle power automatically by asserting DTR -r RESETCMD, --resetcmd RESETCMD shell command for board power-cycling (instead of DTR assertion) -P {stc89,stc12a,stc12b,stc12,stc15a,stc15,stc8,usb15,auto}, --protocol {stc89,stc12a,stc12b,stc12,stc15a,stc15,stc8,usb15,auto} protocol version (default: auto) -p PORT, --port PORT serial port device -b BAUD, --baud BAUD transfer baud rate (default: 19200) -l HANDSHAKE, --handshake HANDSHAKE handshake baud rate (default: 2400) -o OPTION, --option OPTION set option (can be used multiple times, see documentation) -t TRIM, --trim TRIM RC oscillator frequency in kHz (STC15+ series only) -D, --debug enable debug output -V, --version print version info and exit
因此,建议自行使用pip install stcgal
来安装该工具,这样就可以在终端中直接使用stcgal
命令了。
设备连接图如下所示:
图3:USB开发板和开发电脑的连接示意图
对照着原理图,假设TTL转USB设备为A,USB开发板为B,那么连接如下所示:
A的GND连B的任意一个GND。
A的5V连B的任意一个VCC。
A的TXD连B单片机的RXD。
A的RXD连B单片机的TXD。
B单片机的RXD和TXD口,可以参见原理图中J8和J3的10/11口。
如果一切正常,可以在/dev
目录下发现/dev/tty.usbserial-0001
文件,然而,由于使用的TTL转USB的设备不同,导致生成的文件可能不一样,但文件都会处于/dev
目录下,这些文件名通常包含关键字如tty
、usb
等。通过插拔操作,查看/dev
目录下的文件变动,也是一种方法。
如果存在/dev/tty.usbserial-0001
文件,运行以下命令,可以查看单片机是否能正常工作:
$ stcgal -P stc89 -p /dev/tty.usbserial-0001Waiting for MCU, please cycle power:# 程序将会在输入上面的数据后卡住,这时候需要重新拔插VCC线,无所谓是在A设备上还是B设备上,经验来说,拔插B设备上的VCC线最方便。# 如果一切正常,会得到以下输出Target model: Name: STC89C52RC/LE52R Magic: F002 Code flash: 8.0 KB EEPROM flash: 6.0 KBTarget frequency: 22.090 MHzTarget BSL version: 6.6CTarget options: cpu_6t_enabled=False bsl_pindetect_enabled=False eeprom_erase_enabled=False clock_gain=high ale_enabled=True xram_enabled=True watchdog_por_enabled=FalseDisconnected!
2 第一个单片机程序
参考资料
在单片机开发中,实现一个类似于"Hello World"的功能确实比较困难。通常情况下,学习编程语言时我们会写"Hello World",但在单片机开发中,要实现类似功能,需要一系列步骤。
首先你需要一个屏幕,其次必须编写该屏幕的驱动程序,才能在屏幕上输出Hello World
。再简单点,就是通过串口输出Hello World
,但这同样需要编写一个串口驱动程序。
第一个程序就编写驱动是非常不友好的,对于我来说,编写第一个程序的目的是为了了熟悉开发流程,并确保编译、下载和运行的正常执行。因此,我们可以将第一个程序的目标降低到点亮一盏LED灯。
通过原理图可以看出,单片机的P20
到P27
口连接到了LED1
-LED8
这8盏LED灯。这8盏LED灯的另一头连接到了1k欧姆的排阻(RP1)上,而排阻连接到了VCC电源上。所以当P20输出0时,LED1电路就会导通,LED1灯就会发光。
现在,我们编写第一个程序,让P20输出为0,代码如下所示:
// src/main.c#include <8051.h>void main(){ P2_0 = 0; while (1);}
通过PlatformIO IDE
创建的项目目录如下所示:
$ ls -a. .gitignore .vscode lib src.. .pio include platformio.ini test
由于VSCode装了PlatformIO IDE
插件,所以在打开了PlatformIO IDE
项目的情况下,编写好代码后,在左下角找到一个✓图标,点击就可以编译编写好的程序,也可以使用快捷键:shift + cmd + b
。
编译完成后,可以点击build图标右边的→图标,表示将编译好的程序下载到单片机中。在输出行看到Cycling power: done
时,重新拔插VCC
线,就可以下载程序到单片机中了。
如果一切正常,在下载结束后,就可以看到单片机中LED1灯常亮。
在第一个程序写完后,可以查看8051.h
头文件的内容,其中对51单片机的各个端口和寄存器做了宏定义,这样可以方便地控制单片机的各个端口。比如P2_0
就表示原理图中的P20
端口,大小为1bit,P2
表示的是单片机的P20
到P27
端口,大小为1byte。
不同架构的单片机使用的头文件不同,可以通过搜索引擎或者GPT根据芯片型号来找到相应的头文件。在搜索或询问时,记得带上sdcc
关键词。
3 第二个程序——定时器中断
参考资
第二个程序我们来了解一下单片机的定时器中断,不同单片机的定时器中断实现不一样,这个时候需要参考单片机的相关文档,请通过芯片型号+pdf关键字,自行使用搜索引擎获取芯片文档。
首先,需要编写一个定时器的初始化函数,代码如下所示:
//定时器0初始化void Timer0Init(){ TMOD &= 0xF0; //设置定时器模式 TMOD |= 0x01; //设置定时器模式 TL0 = 0x00; //设置定时初值 TH0 = 0x00; //设置定时初值 ET0 = 1; //使能定时器0中断 TR0 = 1; //启动定时器0}
通过直接对寄存器赋值的方式来对单片机进行配置,各个寄存器表示的内容可以参考文档,比如TMOD
寄存器的功能描述如下图所示:
图4:TMOD寄存器功能描述
TR0
寄存器属于TCON
寄存器的比特位,功能描述如下图所示:
图5:TCON寄存器功能描述
ET0
寄存器属于IE
中断寄存器的比特位,功能描述如下图所示:
图6:中断寄存器功能描述
通过对上述三个寄存器的功能描述,可以看出Timer0Init
函数的作用有以下几个方面:
开启定时器0,设置模式1,为16位定时器。
16位定时器使用的计数器为8bit的TL0和8bit的TH0,,因此最大计数次数为65536
次。
TL0和TH0组合成了定时器的计数器T0,每个工作周期,T0 += 1,当T0溢出时,设置TF0寄存器为1,从而触发中断。
定时器的一个重要参数是时间,表示定时器一个循环的时间。通过上面的总结,我们可以知道,TL0和TH0寄存器就是用来控制循环时间。根据文档中一个计算示例,我们可以确定该单片机的时间参数应该如何设置,如下图所示:
图7:定时器时间计算案例
通过USB开发版的原理图,可以看出单片机的X1/X2端口外接了外部晶振,该晶振的频率为22.1184MHz
,因此一个机器周期为:12 / 22118400 ≈ 0.54μs
。
最大循环时间为:65536 * 0.54 ≈ 35389.44μs ≈ 35ms
。
假设定义循环时间为20ms
,那么可以求得T0的计数次数为:20 * 1000 / 0.54 = 36864
。
对计数次数取反,就是TL0/TH0寄存器的设置值:65536 - 36864 = 0x7000
,那么需要设置:TL0 = 0x00, TH0 = 0x70
时间计算完成后,我们开始实现定时器的功能。在本次的测试案例中,我们定义一个走马灯功能,共有8个LED灯,从左向右循环亮起,间隔时间为1s。实现代码如下所示:
#define LEDs P2volatile uint8 times = 0;volatile uint8 TLED = 0b11111110;//定时器0中断void Timer0Isr() __interrupt 1{ // 每20ms触发一次中断 TH0 = 0x70; TL0 = 0x00; // 使LED从左往右闪烁,间隔1s,1s / 20ms = 50次循环 if (times == 50) { times = 0; LEDs = TLED; TLED = (TLED << 1) | (TLED >> 7); // 不建议使用LEDs = (TLED << 1) | (TLED >> 7);因为如果LED坏了会影响LEDs的值 } else { times++; }}
最后,再简单写一个main函数,就可以编译程序了,main函数如下所示:
void Timer0Isr(void) __interrupt 1; //在SDCC编译器中,如果不声明中断,中断不会生效void main(){ // 初始化定时器 Timer0Init(); // 开启中断,EA为IE中断寄存器的比特位,是所有中断的总开关 EA = 1; while (1);}
这样,一个走马灯的程序就开发完了,接着进行编译,下载到单片机中,就可以看到LED灯以1s的间隔,从左往右依次亮起。
4 第三个程序——TTL串口中断
参考资
STC89C52RC
单片机自带TTL串口,可以通过该串口下载程序到单片机中,同样也可以使用串口与单片机通信。
首先来看一下串口的初始化函数,代码如下所示:
#define Fclk 22118400UL#define BitRate 9600UL// 初始化UART配置void InitUART(){ EA = 0; // 暂时关闭中断 TMOD &= 0x0F; // 定时器1模式控制在高4位 TMOD |= 0x20; // 定时器1工作在模式2,自动重装模式 SCON = 0x50; // 串口工作在模式1 TH1 = 256 - Fclk/(BitRate*12*16); // 计算定时器重装值 TL1 = 256 - Fclk/(BitRate*12*16); // PCON |= 0x80; // 串口波特率加倍 ES = 1; // 允许串口中断 TR1 = 1; // 启动定时器1 REN = 1; // 允许接收 EA = 1; // 开启中断}
初始化的各个寄存器同样是通过看芯片文档可以理解各自的用途,如果文档里写的比较简单,可以单独搜索该寄存器,或者询问GPT,都是很容易理解作用的寄存器。
然而,有几个注意事项需要考虑:
Fclk为晶振的频率,BitRate为设置的串口的波特率,在实际开发的过程中发现,可能是为了节省空间,SDCC在编译的过程中把将整型默认设置为short。对于频率和波特率,short
型长度明显不够,所以需要在整型结尾加上UL
表示unsigned long
类型值。
PCON |= 0x80
实际表示的是SMOD = 1
,由于在计算TH1
值的时候使用了除法,可能出现除不尽的情况。而TH1
为1字节的整型,所以在遇到该情况时,可以设置 SMOD=1
,这样有可能能够除尽。
当SMOD=1
时,TH1
的计算公式为:256 - Fclk/(BitRate*12*16)
,当SMOD=0
时,TH1
的计算公式为:256 - Fclk/(BitRate*32*16)
。
接下来是编写串口的中断函数和串口的读写函数,代码如下所示:
volatile char Sending = 0; // 发送标志// UART 接收中断服务void UARTIsr() __interrupt 4{ if (RI) // 当RI = 1时,表示接受到数据,数据储存在SBUF中 { // 清除中断请求 RI = 0; SBUF = SBUF; // 用户输入回显 } if (TI) // 当TI = 1时,表示开始写数据,数据储存在SBUF中 { TI = 0; Sending = 0; }}// 发送一个字符void UartSendChar(char c) { SBUF = c; Sending = 1; //设置发送标志 while(Sending); //等待发送完成}// 发送一个字符串void UartSendString(char *s){ while (*s) { UartSendChar(*s++); }}
接下来可以简单编写一个main函数,代码如下所示:
void UARTIsr() __interrupt 4;void main(){ // 初始化Uart函数 InitUART(); UartSendString("Hello World!\n"); while (1);}
编译程序并将其下载到单片机中后,就可以与USB开发版进行串口通信。串口通信的波特率设置为9600,模式为8N1。每次重置单片机时,都可以在串口中接收到Hello World!
字符串,并且可以看到输入字符的回显,如下图所示:
图8:串口输出
5 第四个程序——检测PDIUSBD12芯片是否正常
参考资学完了前面三个程序后,可以说已经入门了单片机开发,能进行以下几种基础操作:控制端口输出,编写中断函数,通过uart口输出调试信息。
接下来第四个程序,第四个程序的主要任务是让单片机与其他外部芯片进行通信。在这个阶段,我们需要参考原理图,查看PDIUSBD12
芯片的哪些引脚和单片机相连,并且需要参考D12
芯片的参考文档来编写交互代码,简而言之,我们需要实现两种函数:一种用于单片机向D12芯片传输数据的写函数,另一种用于单片机获取D12芯片返回数据的读函数。
首先,参考D12
文档和USB开发版原理图来设置一些宏定义,代码如下所示:
// A0值的宏定义// A0引脚表示数据传输口传输的是数据还是命令,命令为1,数据为0。#define PDIUSBD12_DATA_ADDR 0x00#define PDIUSBD12_CMD_ADDR 0x01// D12的D0-D7引脚为数据传输口,共8位1字节,连接到单片机的P00-P07#define PDIUSBD12_DATA P0// PDIUSBD12芯片与单片机其他连接引脚#define PDIUSBD12_INT P3_2#define PDIUSBD12_A0 P3_5#define PDIUSBD12_WR P3_6#define PDIUSBD12_RD P3_7// 选择命令或数据地址#define D12SetCommandAddr() PDIUSBD12_A0 = PDIUSBD12_CMD_ADDR#define D12SetDataAddr() PDIUSBD12_A0 = PDIUSBD12_DATA_ADDR// WR和RD控制#define D12SetWr() PDIUSBD12_WR = 1#define D12ClrWr() PDIUSBD12_WR = 0#define D12SetRd() PDIUSBD12_RD = 1#define D12ClrRd() PDIUSBD12_RD = 0// 获取中断状态#define D12GetInterrupt() PDIUSBD12_INT// 读写数据#define D12GetData() PDIUSBD12_DATA#define D12SetData(x) PDIUSBD12_DATA = (x)// 将数据口设置为输入状态,51单片机端口写1就是为输入状态#define D12SetPortIn() PDIUSBD12_DATA = 0xFF// 将数据口设置为输出状态,由于51单片机是准双向IO口,所以不用切换,为空宏#define D12SetPortOut()
接着根据下面的并行接口时序图来编写读写函数,时序图如下所示:
图9:并行接口时序图
代码如下所示:
// D12 写命令void D12WriteCommand(uint8 command){ D12SetCommandAddr(); // 表明DATA中的数据为命令 D12ClrWr(); // WR_N置低 D12SetPortOut(); // 将数据口设置为输出状态 D12SetData(command); // 将命令写入数据口 D12SetWr(); // WR_N置高 D12SetPortIn(); // 将数据口设置为输入状态}// D12 写数据void D12WriteData(uint8 command){ D12SetDataAddr(); // 表明DATA中的数据为数据 D12ClrWr(); // WR_N置低 D12SetPortOut(); // 将数据口设置为输出状态 D12SetData(command); // 将命令写入数据口 D12SetWr(); // WR_N置高 D12SetPortIn(); // 将数据口设置为输入状态}// D12 读单字节uint8 D12ReadByte(){ uint8 data; D12SetDataAddr(); // 表明DATA中的数据为数据 D12ClrRd(); // RD_N置低 data = D12GetData(); // 读取数据 D12SetRd(); // RD_N置高 return data;}
接着,通过文档资料发现存在一个ReadID = 0xFD
命令,可以获取D12
芯片的ID编码,以此来判断芯片是否能正常工作,ID
值为固定的0x1012
。根据该信息,编写以下代码:
// PDIUSBD12芯片读ID命令#define D12_READ_ID 0xFD// D12 执行ReadID命令uint16 D12ReadID(){ uint16 id; D12WriteCommand(D12_READ_ID); // 执行ReadID命令 id = D12ReadByte(); // 读取ID低字节 id |= (D12ReadByte() << 8); // 读取ID高字节 return id;}
获取到ID值后,打算通过串口把ID值输出,需要编写一个输出整型的函数,代码如下所示:
__code uint8 HexTable[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};// 将short转换为hex字符串void UartSendShortToHex(uint16 x){ uint8 i; uint8 output[7] = {0,}; output[0] = '0'; output[1] = 'x'; for (i = 5; i >= 2; i--) { output[i] = HexTable[x & 0x0F]; x >>= 4; } UartSendString(output);}
最后编写main函数,读取D12
芯片的ID并且输出,代码如下所示:
void main(){ uint16 idv; // 初始化Uart函数 InitUART(); UartSendString("Hello World!\r\n"); // 获取芯片ID idv = D12ReadID(); // 输出芯片ID UartSendString("Get PDIUSBD12 ID: "); UartSendShortToHex(idv); UartSendString("\r\n"); while (1);}
编译以上代码,并且下载到单片机当中,得到结果如下图所示:
图10:示例四输出结果
得到以上结果,说明D12芯片一切正常,可以进行后续开发工作。后续的USB开发工作将在后续文章中继续讲解。
6 参考链接
参考资学完了前面三个程序后,可以说已经入门了单片机开发,能进行以下几种基础操作:控制端口输出,编写中断函数,通过uart口输出调试信息。
[1]https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide
[2] https://github.com/grigorig/stcgal
作者名片
往 期 热 门
(点击图片跳转)
戳“阅读原文”更多精彩内容!