⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 firmware.txt

📁 USB驱动使用比较详细的一个例子
💻 TXT
字号:
MiniNurse(MN)固件源代码及其分析

    对于像c51这样简单的芯片,其固件就相当于操作系统,一个相当原始和粗糙的操作系统。下面是一个"Hello World"性质的固件片断(C for51),我做了一些注释:

void main (void)	//主循环。51芯片加电后PC为0,在这里有各种寄存器的初始化代码,然后就转到这里执行。
{
	/*这里可能会有中断、定时器、等的初始化代码*/

	while (1)	//注意!这是一个死循环,这正是固件成为操作系统的原因。
{		//如果没有这个主循环,执行完后面的代码后,芯片就会处于空闲状态。只有重置或者重新上电才能唤醒它。

	P1 ^0= 0x01;		/*这两行就是我们的示例代码,它们是唯一的“进程”,会一直被执行,	*/
	printf ("Hello World\n");	/*printf()通常会从51的串口输出。			*/
}

    MiniNurse的固件要比这复杂。MN的固件是分层的,这对固件的可理解性、可移植性以及健壮性都非常重要。比如,如果换用Motorola或者其它种类的单片机,只需要修改最底层的代码就可以了,这个工作量是非常小的。又如要扩展MN的功能,只要添加相应的厂商请求部分以及可能的主循环(mainloop.c)代码,这个工作量会因为扩展的功能而异,但如果不是分层,代码的移植将会是可怕的。
    MN的固件代码,分成以下几个层次:硬件抽象层,D12命令接口,中断服务例程,USB标准和厂商请求,主循环。

硬件抽象层:base_io.c base_io.h
    该层定义了C51和D12通讯的方法。base_io.h中声明了两个函数:
	void outportb(unsigned int Addr,unsigned char Data);
	unsigned char inportb(unsigned int Addr);
    outportb()为发送信息到D12,inportb()相反。
    51和D12之间的信息有数据和地址之分。在51看来,它连接的是一个特殊的扩展RAM,这个RAM只有两个地址状态。这个两个地址状态对于D12来说则意味着下次到达的信息是数据还是对D12的命令。D12的手册上说,偶数地址为数据,奇数地址为命令。参考outportb()的实现:
void outportb(unsigned int Addr,unsigned char Data) {
	*((unsigned char xdata *)Addr)=Data;
	}
在base_io.h中,有这么两行:
#define D12_Command	0xff03
#define D12_Data		0xff02
在更高层的代码中,会经常看到类似如下的调用:
outportb(D12_Command, 0xFD);
这条语句说明0xFD是对D12的一个命令(读芯片ID)。实现中的xdata是C for 51的扩展,被这个关键词修饰的变量存储在外部RAM中,前面说过,D12就相当与51的一个扩展RAM,所以这里就是在D12中。C for 51还有其它的扩展,可以参阅具体的编译器文档,它们之间有细微差别,本文只在必要的时候做必要的解释。
    inportb()的实现方法与outportb()绝类,只是方向相反。
    D12除了支持上面使用的这种所谓“复用方式”外,还支持非复用方式。这种方式通过控制D12上的A0管脚区分指令和数据。具体的参看D12手册和硬件设计文档。这种方式下需要修改outportb()和inportb()的实现,代码和注释如下。不过这种方式并没有试验成功,目前还不确定是否是固件的问题,仅示意!语言顺序和那些看上去没有意义的语言成份都是试图符合D12的时序要求。

void outportb(unsigned int Addr,unsigned char Data)	//send to D12
{
	switch (Addr)	//in fact,'Addr' should be described as 'Command or Data select word'
	{
		case D12_Command:	//send command-byte to D12
			A0c=D12_Command;
			break;
		case D12_Data:	//send data-byte to D12
			A0c=D12_Data;
			break;
	}

	WRc=0;	//Write immediately
	do {if (1+1==2);} while(0);
	P0=Data;
	do {if (1+1==2);} while(0);
	WRc=1;	//enclose
}

unsigned char inportb(unsigned int Addr)	//receive from D12
{
unsigned char tmpData;
	switch (Addr)	//in fact,'Addr' should be described as 'Command or Data select word'
	{
		case D12_Command:
			A0c=D12_Command;
			break;
		case D12_Data:
			A0c=D12_Data;
			break;
	}
	
	RDc=0;	//Read immediately
	do {if (1+1==2);} while(0);
	tmpData=P0;
	do {if (1+1==2);} while(0);
	RDc=1;	//enclose
	return tmpData;
}


D12命令接口:D12_comm_if.c D12_comm_if.h
    D12向外提供了十数条指令,芯片手册上有详细的列表和语法说明。D12_comm_if.h声明了它们中的大多数,因为有几条指令并没有多少用处。D12_comm_if.c中的实现完全根据芯片手册的定义而写,没有什么可以说的。仍以读芯片ID的例子做示范性说明,这个命令似乎也没有实际的用途,不过却可以知道D12是否正常工作。
    读芯片ID的命令号是0xFD,这个刚才看过了。该指令后跟两个八位数据,方向是D12-->51。在实现中,可以看到发送命令后,读了两次数据。第一次读到的是低位,第二次是高位,做了位操作后拼装成一个16位整型返回。(是的,16位。8位单片机还能怎样?)为了方便,将代码再列下:
unsigned short D12_ReadChipID(void)
{
	unsigned short tmpi,tmpj;

	if(bEPPflags.bits.in_isr == 0)
		DISABLE;

	outportb(D12_Command, 0xFD);
	tmpi=inportb(D12_Data);
	tmpj=inportb(D12_Data);
	tmpi += (tmpj<<8);

	if(bEPPflags.bits.in_isr == 0)
		ENABLE;
	return tmpi;
}

中断服务例程:isr.c
    再介绍一个C for 51的扩展,将一个函数声明为中断服务例程的方法是:
void FUN_ISR (void) interrupt N using M;
其中N是该函数将要响应的中断号,常用的中断号是
0<-->外部中断0
1<-->定时器中断0
2<-->外部中断1
3<-->定时器中断1
4<-->串口中断
    一些51变种有更多的中断向量可用,不论。M对应51的通用寄存器组号。ISR的工作就是在全局变量bEPPflags的某些域中记录下中断来源,对于伴随后USB数据而产生的中断,ISR会对操作D12将数据从D12的内部缓冲区读入主存(ControlData)并做适当的解析。相对于其它模块,ISR的流程图是最复杂的(得感谢USB-IF,是他们使得最复杂的部分也不那么可怕^_^)。
    首先是usb_isr() interrupt 0 {}声明中断,因为根据连线,D12的数据中断被引到51外部中断0。ISR里做的第一件事情是禁止所有中断(ENABLE/DISABLE是定义在base_io.h里的宏)。因为没有必要嵌套中断,而且这样做会带来复杂性。在操作系统教科书里能看到类似的解释。
    在这个ISR里,可以处理如下事务:总线挂起、端点0的IN和OUT,端点1的IN和OUT。因为没有使用DMA和端点2,所以有时候即使声明了它们,定义部分也会是空的。
    端点0的IN包处理。IN包的方向是从设备到主机。所以在MCU看来处理函数的名字是ep0_txdone()。它的代码如下:
void ep0_txdone(void)
{
	short i = ControlData.wLength - ControlData.wCount;

	D12_ReadLastTransStatus(1); // Clear interrupt flag

	if (bEPPflags.bits.control_state != USB_TRANSMIT)
		return;

	if( i >= EP0_PACKET_SIZE) {
		D12_WriteEndP(1, EP0_PACKET_SIZE, ControlData.pData + ControlData.wCount);
		ControlData.wCount += EP0_PACKET_SIZE;

		bEPPflags.bits.control_state = USB_TRANSMIT;
	}
	else if( i != 0) {
		D12_WriteEndP(1, i, ControlData.pData + ControlData.wCount);
		ControlData.wCount += i;

		bEPPflags.bits.control_state = USB_IDLE;
	}
	else if (i == 0){
		D12_WriteEndP(1, 0, 0); // Send zero packet at the end ???

		bEPPflags.bits.control_state = USB_IDLE;
	}
}

    D12_ReadLastTransStatus();实现D12的读最后状态寄存器命令,该命令可以清除D12内部寄存器的中断位,这样D12可以继续处理来自USB的数据。之后判断待传送的数据是否大于端点0的的包大小,如果大于,则要分多次传送,用bEPPflags.bits.control_state域标志记录一个逻辑上连续的数据是否传送完成。D12_WriteEndP();实现了D12写端口命令。关于这个命令的参数及其实现可以查看D12命令接口层和D12的手册。

    端点1的OUT包处理。OUT和IN包方向相反,对于MCU来说,对这种包的处理是从D12读取数据。这段代码不在此列出。具体流程如下:
	1 和IN包处理一样,通过读取最后状态,返回中断源,清除相应中断位。
	2 判断包是否为SETUP包。USB规范中,SETUP事务包通常包含了对设备的控制命令。如果读入正确,固件会做一组高低位转换工作,多数iNTEL处理器的字位顺序和网络通讯中的字位顺序是颠倒的。在iNTEL计算机上用socket编程也常需要转换。
	3 之后需要对OUT包向USB主控发出响应,表明接受成功。
	4 对于SETUP包,固件把各个域复制到全局变量复制到ControlData中;对于非SETUP包则只复制数据。
    端点1的处理端点0的类似,并且可能没有SETUP的处理还会更加简单。

USB标准请求:usb_standard_request.c usb_standard_request.h
    USB1.1规范定义的十一个所有USB设备必须支持的命令(当然,有一两个不支持无碍),当前MN就只实现了九个:
	void get_status(void);
	void clear_feature(void);
	void set_feature(void);
	void set_address(void);
	void get_descriptor(void);
	void get_configuration(void);
	void set_configuration(void);
	void get_interface(void);
	void set_interface(void);
    所有这些命令均按照USB的规范实现,基本上可以算做“标准代码”。在此不做多议。
    在usb_standard_request.c中同时定义了一些数据结构,它们用code修饰,表明这些信息将会保存在MCU的程序存储器里(ROM)。这些数据结构是重要的。在USB术语中称其为描述符。它们会在设备枚举时由主控读取。
    描述符是结构化的,一个物理上存在的USB设备有且仅有一个设备描述符,这里的实例为DeviceDescr,它描述了MN的全局性信息:这是一个USB1.1设备,它的类是0xDC(没有意义),端点0的包大小为16字节,厂商ID是菲利普等等。
    一个设备描述符之下可以超过一个配置描述符。一个配置描述符通常表示一个逻辑上的设备。MN只有一个配置描述符,实例为ConfigDescr。配置描述符有更细节的参数,如MN有一个接口,支持自供电和远程唤醒,最多可以从总线上攫取100mA电流等等。
    每个配置描述符又可以有多个接口描述符作为补充。接口代表一个具体的功能,在Linux中,驱动最终落实在接口上。MN有一个接口描述符实例InterfaceDescr。它指出MN的端口数,端口的协议等等。
    端点是USB的原子通信单位,一个或多个端点组成接口(即功能)。端点很像传统外设中的端口寄存器。当USB主控以及操作系统和USB设备通讯时,实际上总要和总要某个端点通讯。人们把这个端点和主控之间的信道抽象,并称之为管道。端点是有方向的,或者IN或者OUT或者双向端点。端点0是默认端点,所有设备必须有端点0用于设备枚举。端点描述符中的最大数据包长度很像计算机网络中的MTU,表示一次的最大传输量。
    MN有四个端点描述符实例,分别是端点1和2的IN/OUT。

USB厂商请求:usb_usr_request.c usb_usr_request.h
    这段代码很短,但却是MN独有的。目前MN使用厂商请求(而不是类命令)完成其全部功能:对LED(或者开关)的控制。
    USB1.1中规定命令格式如下:
    可以看出在厂商请求下有足够的空间发挥。MN只使用了bRequest和wIndex。bRequest作为命令号:0为打开LED,1相反。bRequest有一个字节的长度,也就是说有256条命令号可以选择,按照对MN的幻想我预定义了用于MN上LCD、SPEAKER等等的命令号。wIndex在LED命令集合中作为LED的序号。为了方便地控制LED,我定义了一套LED库,由myLED.c和myLED.h组成。这两个文件注释足够说明它们。

主循环:mainloop.c mainloop.h
    进入主循环,首先是初始化,当然这个工作也可以交给编译器自动完成,但是有些参数还是要自己选择。
    之后会碰上函数reconnect_USB();。D12提供一个很有意思的能力:soft_connect。MCU可控制D12何时连接到USB总线上。原理是:USB通过D+/-上的电平变化侦测新设备,并要求高速设备在D+上有一个1.5K欧姆的上拉,低速设备在D-上有上拉。而D12内部提供一个D+上的上拉电阻,通过指令可以现在是否连通。这样可以防止MCU在尚未初始化前,D12提前对USB响应。对我来说,只要按下MCU的重启键而不是重新拔插接头就进行固件与驱动联合测试,十分方便。
    因为完全用厂商请求完成操作,所以主循环相对简单:反复检查标志位。只介绍几个重要成份。
    void control_handler();这个函数用来判断SETUP包是USB标准请求还是厂商请求。并把参数传递给相应的组件。这里有两个函数跳转表,分别用于厂商请求和标准请求。定义在文件的开头部分。

两个重要的全局数据结构和整体结构
     定义于mainloop.h中的数据结构EPPFLAGS(及其实例:bEPPflags)和CONTROL_XFER(及其实例:ControlData),始终贯穿于整个固件。前者定义了各种事件并作为事件标记,后者保存来自D12内部缓冲区的数据。
    把D12命令接口层和硬件抽象层看成最底层,中断服务例程是中间层,主循环和两个请求处理是上层,它们之间存在如下调用关系:
    

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -