1 串口API
在Linux中,操作设备的统一接口就是:open/read/ioctl。
行规层(Line discipline):接受驱动程序的数据,再传递给应用程序;它可以对数据进行处理,如回写等。默认情况下,行规层收到数据就保存下来,直到收到回车再传给数据。
UART编程套路:
1、open。
2、设置行规层:波特率、数据位、停止位、校验位、RAW模式,有数据就返回。
3、read/write。
串口结构体termios :该结构体控制行规层,控制硬件。
struct termios {
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_cc[NCCS]; /* control characters */
cc_t c_line; /* line discipline (== c_cc[19]) */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
可以使用ioctol,但是已经封装好了函数。
2 应用程序部分
2.1 open(set)_port:打开串口并设置属性
这里面传的参数com是设备节点;因此需要先通过驱动程序创建设备节点。
fd = open_port(argv[1]);
int open_port(char *com)
{
int fd;
//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
fd = open(com, O_RDWR|O_NOCTTY);
if (-1 == fd){
return(-1);
}
if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
{
printf("fcntl failed!\n");
return -1;
}
return fd;
}
iRet = set_opt(fd, 115200, 8, 'N', 1);
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
if ( tcgetattr( fd,&oldtio) != 0) {
perror("SetupSerial 1");
return -1;
}
bzero( &newtio, sizeof( newtio ) );
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
newtio.c_oflag &= ~OPOST; /*Output*/
switch( nBits )
{}
switch( nEvent )
{}
newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间:
* 比如VMIN设为10表示至少读到10个数据才返回,
* 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
* 假设VTIME=1,表示:
* 10秒内一个数据都没有的话就返回
* 如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
*/
fcntl(fd, F_SETFL, FNDELAY) 读数据时不等待,没有数据就返回0;
fcntl(fd, F_SETFL, 0) 读数据时,没有数据阻塞。
newtio.c_cc[VMIN] 读到VMIN个数据才返回,如果为0,有数据就返回。
2.2 main 函数
int main(int argc, char **argv)
{
int fd;
int iRet;
char c;
if (argc != 2)
{
printf("Usage: \n");
printf("%s </dev/ttySAC1 or other>\n", argv[0]);
return -1;
}
fd = open_port(argv[1]);
if (fd < 0)
{
printf("open %s err!\n", argv[1]);
return -1;
}
iRet = set_opt(fd, 115200, 8, 'N', 1);
if (iRet)
{
printf("set port err!\n");
return -1;
}
printf("Enter a char: ");
while (1)
{
scanf("%c", &c);
iRet = write(fd, &c, 1);
iRet = read(fd, &c, 1);
if (iRet == 1)
printf("get: %02x %c\n", c, c);
else
printf("can not get data\n");
}
return 0;
}
3 行规层问题
如果最小字节数设置0:
newtio.c_cc[VMIN] = 0;
那么read的时候不管有没有数据都会返回,由于串口速度慢与程序执行速度,所以这个时候经常读不到数据;
需要大最小字节数设置为1
newtio.c_cc[VMIN] = 1;
这样,read函数起码需要收到1个数据才会返回。言外之意就是会等待串口通讯。
补充引脚、电路图等
确定设备节点对应的是那个引脚
可以查到ttymxc5的主次设备号,但是不能确定它是哪一个UART。
/proc 目录
proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口,可以在运行时访问内核内部数据结构、改变内核设置。
用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是动态从系统内核读出所需信息并提交的。
另外,在/proc下还有三个很重要的目录:net,scsi和sys。Sys目录是可写的,可以通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi目录不存在。
可以通过proc文件系统查询到tty设备的信息,可以发现ttymcx0、tymcx2、tymcx5三个UART设备的映射地址;查阅芯片手册
它们分别对应UART1、3、6。
ttymxc5对应的引脚
通过查芯片手册UART6是这两个引脚的复用;
查看电路原理图找到对应的外部引脚(实际上还要往上查到核心板,但是这里省略了)。
- 这些引脚为什么叫这个名字?外部引脚在电路图上所用名字是该引脚的默认功能。
- 当你借由驱动使用这个引脚时,驱动程序会改变这个引脚的寄存器,从而呈现UART6的特性。
- UART的驱动在设备树中完成,如果想设置新的UART串口需要在设备树中编译。
SMbus
I2C的子集,相较于I2C有更高的要求。
SMbus必须要回应信号。
3 I2C
3.1 I2C主要的结构体
1、需要确定是哪一个I2C控制器;控制器 (I2C_adpater) 提供读写能力,有传输函数。
2、从设备结构i2c_client,主要是 设备地址addr,和主设备I2C_adpater。
3、传输的数据i2c_msg结构体,主要是buf、length、addr(设备地址信息),一共两个msg,一个接一个收
3.2 i2ctools
三个问题:使用那个控制器、访问那个设备、读写什么数据
-
怎么指定I2C控制器?
i2c-dev.c提供为每个I2C控制器(I2C Bus、I2CAdapter)都生成一个设备节点:/dev/i2c-0、/dev/i2c-1等
open某个/dev/i2c-X节点,就是去访问该I2C控制器下的设备 -
怎么指定I2C设备:通过ioctl指定I2C设备的地址?
1、ioctl(file, I2C_SLAVE, address)如果该设备已经有了对应的设备驱动程序,则返回失败
2、ioctl(file, I2C_SLAVE_FORCE, address)如果该设备已经有了对应的设备驱动程序,但是还是想通过i2c-dev驱动来访问它则使用这个ioctl来指定I2C设备地址 -
怎么传输数据?
两种方式
一般的I2C方式:ioctl(file, I2C_RDWR, &rdwr)
SMBus方式:ioctl(file, I2C_SMBUS, &args)
3.3 举个栗子AT24C20
- 确定设备的地址,查芯片手册
因此地址为**1010000 0x50**
2、写操作
开始-设备地址-写的地址-数据
对应
3、读操作
开始-设备地址-写-读的地址-开始-设备地址-数据
符合SMBus协议:i2c_smbus_write_byte_data()。
int main(int argc, char **argv)
{
unsigned char dev_addr = 0x50; 设备地址
unsigned char mem_addr = 0; 读取的位置
unsigned char buf[32];
int file;
char filename[20];
unsigned char *str;
/* 打开设备节点 */
file = open_i2c_dev(argv[1][0]-'0', filename, sizeof(filename), 0);
/* 设置地址 */
if (set_slave_addr(file, dev_addr, 1))
{
printf("can't set_slave_addr\n");
return -1;
}
4 SPI 使用GPIO模拟
- SS:片选引脚,确定选择到哪一个从设备。选定时保持低电平并传输数据;
- SCLK:时钟,上升沿还是下降沿采样;
- MOSI:输出,MISO:输入。都是对于主设备而言的输入还是输出
4.1 举个例子
解决问题:
- 使用GPIO模拟SPI
- 如何传输数据
4.1.1 GPIO模拟SPI
spi-gpio.c
#include"xxxx.h" /* 头文件包含寄存器信息 */
/* 使用GPIO模拟SPI */
static void SPI_GPIO_Init(void)
{
/* 把所涉及的GPIO引脚设置为输入输出模式 */
/* GPFCON即是GPIO的控制寄存器 */
GPFCON &= ~(3<<(1*2)); //设置GPG2 为输出
GPFCON |= ~(1<<(1*2)); //设置GPG4 为输入
/* GPG2 FLASH_CSn output
GPG4 OLED_DC output
GPG5 SPIMISO input
GPG6 SPIMOSI output
GPG7 SPICLK output
*/
GPFCON |= ~(3<<(2*2) | 3<<(2*2)| 3<<(2*2)|3<<(2*2));
.......
}
void SPIInit(void)
{
SPI_GPIO_Init();
}
void SPISendByte(unsigned char val)
{
int i;
for(i = 0; i< 8; i++)
{
SPI_Set_CLK(0); // 数据锁存是上升沿触发的,需要自己给时钟引脚创造上升沿
SPI_Set_DO(val & 0x80); //保留第一位
SPI_Set_CLK(1); // 数据锁存是上升沿触发的,需要自己给时钟引脚创造上升沿
val <<= 1;
}
}
static void SPI_Set_DO(char val)
{
/* GPGDAT第六位是输入,主机的输出 */
if(val)
GPGDAT |= (1<<6); //或1 保证绝对为1
else
GPGDAT &= ~(1<<6); //与0 保证绝对为0
}
关于静态函数:
- 静态函数它只能在声明它的文件当中可见,不能被其他文件可用。
- 可以直接使用类名+静态成员名访问此静态成员,因为静态成员先于类的声明而存在于内存,也可以根据类声明的对象来访问。而非静态成员必须实例化之后才会分配内存。
oled.c
void OLEDInit(void)
{
/* 如何初始化OLED屏幕需要查看OLED芯片手册,里面会提供 */
.......
}
static void OLEDwrite(void)
{
/* OLED多出一个DC引脚,高电平表示传入的是命令,低电平为数据
因此一定要看看芯片手册里面怎么写,芯片引脚干嘛的。
*/
OLED_Set_DC(0);/* 说明传入的是命令 */
OLED_Set_CS(0);/* 片选引脚为低电平,表示芯片被选用上 */
......
OLED_Set_DC(1)
OLED_Set_CS(1);
}
/* 本质上就是改变GPIO数据寄存器的值,来改变引脚输出数据 */
/* 将相关的输出GPIO引脚赋值,因此是GPFDAT寄存器组 */
static void OLED_Set_CS(char val)
{
if(val)
GPFDAT |= (1<<1);
else
GPFDAT &= ~(1<<1);
}