ISP 在线系统编程

这个问题也是在学单片机开始前就在考虑的,不过本着先学习后实践的目的就搁浅了,这里总结一下。

发展到如今(2019年)常用的方式是通过“在线系统编程(ISP)”这种方式来把程序写入到芯片(MCU)里,这种方式的好处是芯片不需要取出,这里我理解的在线两个字是电路。也就是说即使你的芯片已经焊接在电路版上也不同取下来通过“编程器”来进行程序写入。这里又引申出来几个协议,这些协议是按照电气标准及协议来区分的比如:RS232、RS422、UART等,这里的UART另一种叫法是TTL,其中常用的是RS232和UART(TTL)。

主要的区别RS232是负逻辑电平,5~12v是低电平,而-12~-5v是高电平。而TTL也就是UART没有那么多区分,它的电平只是5V。


网上很多的USBISP、USBASP实际就是USB接口转TTL或RS232协议这样可以对芯片进行程序写入。开始是只有USBISP也就是通过USB接口去实现转换相应的TTL或者RS232协议来达到程序写入的目的。而USBASP查阅了很多资料,也没有发现他俩的区别,不过在USBASP的官网倒是大致看出点眉目。实际USBASP和USBISP在电路设计上是一模一样的,提一点的是不论是USBASP还是USPISP他们都用到了AVR芯片。而USBASP只是一个固件程序,就是将名叫USBASP的固件写入到USPISP的电路里,这个电路里有一个AVR芯片。如果你将USBASP这个固件写到了USPISP这个电路中,那么你就可以通过Progisp这类软件来进行程序烧写。否则你只能通过AVR STUDIO这个程序来进行烧写。在我的理解看来不论是USBISP这个电路,或者是USBASP这个固件。他们本质上都是上面提到的“编程器”即你必须取下MCU芯片,通过引线或者电路中预留好的接口槽进行芯片写入。没有达到ISP可在线系统编程这种思想。他们的大致关系是

屏幕快照 2019-11-07 下午10.26.32.png


以上是对USBASP和USBISP一些解释,以及对“编程器”的一些关联想法,有了这个固件(USBASP)以后可以更好的支持一些协议或者加快写入速度。


对于“在线编程系统”如何实现,实际已经有很成熟的方案,即在电路里增加一块USB转串口的芯片。通过这个芯片达到数据传送到MCU里。因为你的电脑或者其他设备的USB接口是无法直接驱动芯片的。这里就要介绍两个能直接放在电路里的转换芯片。如:MAX232和CH340等。

MAX232是可以将TTL信号转换为RS232信号,或者RS232信号转TTL信号的。不过它的体积相对大一些,焊接在电路中比较占用位置。

CH340是将其他信号转换为TTL信号,体积相对较小,而且各种MCU芯片也相对应的支持TTL信号,TTL的信号线是MCU上的TXD和RXD。

使用上述两个芯片,就不用去做“编程器”了,一般像51单片机就是用的CH340芯片来进行和USB接口通讯写入程序的,注意的是这种情况下,你的整个电路要上电,否则是没办法写入的。而“编程器”则不需要给MCU供电,直接用引线连接到“编程器”上,“编程器”自己就可以供电给它并进行写入。

更为详细的解释,可以自行网上搜索。这里也给出一个建议:如果你的电路是在洞洞板或者面包板上进行实践的,我建议买一个“编程器”某宝搜索“USBASP或USBISP”关键字即可,10块左右。如果你的电路是要制作成PCB并且要进行调试的,我建议你买一块USB转串口芯片,直接焊在电路里依旧是10块左右。


以上仅为个人理解,如有错误还望指出。

ds18b20使用方法

ds18b20是一个比较不错的温度芯片,它是使用了一根线来实现(DS)接口。具体的可以查看相关文档。

这里主要说一下连线方式和怎么读取,连线方式如下图:

微信截图_20191105101508.png

注意这里接了一个4.7Kohm的电阻,这是一个上拉电阻,主要用于数据的传输。对于为什么要加电阻以及详细的解释可以看下面的讨论。如果不加这个电阻是无法进行有效的数据传输的。一开始就是在这里没有解决。

关于从地址的说明如下:

Read ROM(读 ROM) [33H]

Match ROM(匹配 ROM) [55H]

Skip ROM(跳过 ROM] [CCH]

Search ROM(搜索 ROM) [F0H]

Alarm search(告警搜索) [ECH]

Read ROM(读 ROM) [33H]

Match ROM(匹配 ROM) [55H]

Skip ROM(跳过 ROM] [CCH]

Search ROM(搜索 ROM) [F0H]

Alarm search(告警搜索) [ECH]

效果如下

微信截图_20191105102808.png

在DS18B20说明上VCC的位置应该是VDD是数字电源,VCC是模拟电源。这里直接将它俩都接在了模电上,因为我看很多电路图都是如此接法。具体的vcc,vdd的区别可以查阅相关资料。


以上仅为个人理解,如有错误还望指出。

PCB时钟DS1307

考虑到很多人是使用C语言来写的单片机,而我是用的basic语言来写的,所以就不贴代码了。效果图如下

屏幕快照 2019-10-30 下午7.41.57.png

连线方式:所有的时钟总线和数据总线连接到一起,接到MCU的引脚上。

读写方式:通过每个芯片不同的地址来进行数据的写入和读取。

epoll 电平触发、边沿触发

看视频里有一句话有疑问,所以看了下电平触发和边沿触发。这句话是:“在复用IO里必须使用阻塞模式”这句话是错误的

epoll 对文件描述符有两种操作模型:电平触发(LT)、边沿触发(ET)。select 和 poll 模型只有电平触发。

电平触发:也就是只有高电平(1)或低电平(0)时才触发通知。

边沿触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知。

1571123733(1).png

由于上升沿和下降沿是电压发生变化而产生的,所以可以理解为边沿触发只有发生变化才会触发

而高电平低电平是可以一直保持的,可以看到高电平②是比高电平①要长的,是因为IIC传输数据的时候是两条线,有一条时间线,所以在一段时间内可以一直高电平或者低电平也没事。所以电平触发会一直通知,直到状态发生改变。由高电平转到低电平,由有数据变为无数据,否则会一直提示即使缓冲区里面的数据没有读完也是会提示你继续读取。

这里只是以电平的转换过程来理解LT和ET模式,并不是说功能的实现是高低电平,只是一种理解方式或者说是实现的原理上差不多。

电平触发指的是:当存在一个读写事件的时候,epoll会发送消息,也就是会从睡眠状态中醒来,然后继续走代码,如果没有事件发生,会一直堵塞,下面的代码不会触发。如果这个事件到来,比如读事件到来后,可以暂时不做处理,当下次循环的时候,会继续触发这个事件,可以在读取。直到内存中的数据被你读完。这个读的事件在下次循环的时候才不会触发。否则会一直触发。


边沿触发和电平触发恰恰相反,如果你这次事件不处理,那么下次这个事件不会继续通知。所以在性能方面边沿触发要更好一些,因为从睡眠醒来的次数比较少。边缘触发注意的是:如果你这次事件读取的内容没有读取完成,还剩余一部分,那么下次不会触发读事件,但是如果有新的数据写入到文件描述符,那么读事件会再次触发。


水平触发(原文)

1. 对于读操作

只要缓冲内容不为空,LT模式返回读就绪。

2. 对于写操作

只要缓冲区还不满,LT模式会返回写就绪。

边缘触发

1. 对于读操作

(1)当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。

(2)当有新数据到达时,即缓冲区中的待读数据变多的时候。

(3)当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

2. 对于写操作

(1)当缓冲区由不可写变为可写时。

(2)当有旧数据被发送走,即缓冲区中的内容变少的时候。

(3)当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

epool 更改LT、ET模式

epoll事件结构

    typedef union epoll_data {
        void    *ptr;
        int      fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    struct epoll_event {
        uint32_t     events;    /* Epoll events */
        epoll_data_t data;      /* User data variable */
    };

events可以包含两种:

LT:EPOLLIN(默认模式)

ET:EPOLLET

填充相应的方式即可更改触发模式。

代码解释

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}
void addfd( int epollfd, int fd, bool enable_et )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if( enable_et )
    {
        event.events |= EPOLLET;
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, false );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
            memset( buf, '\0', BUFFER_SIZE );
            int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
            if( ret <= 0 )
            {
                close( sockfd );
                continue;
            }
            printf( "get %d bytes of content: %s\n", ret, buf );
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, true );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
                // ET模式下,需要循环读取数据。
                //直到返回EAGAIN,因为下次不会通知。
            while( 1 ) 
            {
                memset( buf, '\0', BUFFER_SIZE );
                int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
                if( ret < 0 )
                {
                    if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
                    {
                        printf( "read later\n" );
                        break;
                    }
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );
                }
                else
                {
                    printf( "get %d bytes of content: %s\n", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );
    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
    ret = listen( listenfd, 5 );
    assert( ret != -1 );
    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd, true );
    while( 1 )
    {
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        lt( events, ret, epollfd, listenfd );
        //et( events, ret, epollfd, listenfd );
    }
    close( listenfd );
    return 0;
}

关于触发模式的选择

1.对于监听的sockfd,最好使用电平触发模式,边沿触发模式会导致高并发情况下,有的客户端会连接不上,因为事件可能只会触发一次。如果非要使用边沿触发,网上有的方案是用while来循环accept()。

2.对于读写的connfd,电平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞,因为有的时候,电平触发已经发送事件,但是这个时候缓冲区的数据还未写,调用recv的时候有可能会出现堵塞状态,这是一种极端情况。

3.对于读写的connfd,边沿触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据,因为如果设置成堵塞,你第一次数据没有读写完,那么不会再次epoll事件,你的recv会一直堵塞下去,直到有新的事件产生,你的recv才会再次收到事件,然后读写数据。如果是非堵塞的话,那么可以用while(1)来一直读数据。而不必等待事件


ssd1306显示汉字

屏幕快照 2019-10-06 下午3.54.53.png

我是利用avr来驱动的,关于avr如何连接ssd1306。大家可以翻看手册,能够更好的理解。这里我选择的是IIC的连线方式,所以ssd1306的D1和D2我连在一起了,这也是手册里推荐的方式。

这里我选择的编译器是bascom,听说AVR STUDIO更好并且免费。bascom是收费的,所以使用前你需要购买,网上的破解版我基本上都有尝试,基本上无法使用,因为版本太低。高版本的只提供demo只能编译4K一般够用。

由于内部的驱动方式,什么屏幕点亮,清理内存,复位屏幕。等等一些操作,如果你的线连接的方式对,那么bascom会自动帮你整好整个这一套流程,不然你就得按照手册一步一步去操作。代码直接使用bascom官网提供的:

'-------------------------------------------------------------------------------
'                       SSD1306-I2C.BAS
'                     (c) MCS Electronics 1995-2015
'          Sample to demo the 128x64 I2C OLED display
'
'-------------------------------------------------------------------------------
$regfile = "m88pdef.dat"
$hwstack = 32
$swstack = 32
$framesize = 32
$crystal = 8000000
Config Clockdiv = 1                                         ' make sure the chip runs at 8 MHz
 
Config Scl = Portc.5                                       ' used i2c pins
Config Sda = Portc.4
Config Twi = 400000                                         ' i2c speed
 
I2cinit
$lib "i2c_twi.lbx"                                         ' we do not use software emulated I2C but the TWI
$lib "glcdSSD1306-I2C.lib"                                 ' override the default lib with this special one
 
#if _build < 20784
Dim ___lcdrow As Byte , ___lcdcol As Byte                 ' dim these for older compiler versions
#endif
 
Config Graphlcd = Custom , Cols = 128 , Rows = 64 , Lcdname = "SSD1306"
Cls
Setfont Font8x8tt                                           ' select font
 
Lcdat 1 , 1 , "BASCOM-AVR"
Lcdat 2 , 10 , "1995-2015"
Lcdat 8 , 5 , "MCS Electronics" , 1
Waitms 3000
 
Showpic 0 , 0 , Plaatje
 
End
 
 
$include "font8x8TT.font"                                   ' this is a true type font with variable spacing
 
 
Plaatje:
  $bgf "ks108.bgf"                                         ' include the picture data

这里的glcdSSD1306-I2C.lib库就是一个封装好的ssd1306库。如果你大致能看懂的话,那么你可以继续看,如果不行的话,那就去查些资料吧。首先要知道一点,一个汉字或者字符显示在屏幕上有这么几个要素,宽度,高度,块大小(字体)。这些东西可以去看字体方面的知识点。还有阴码,阳码,扫描方式。bascom显示字符利用了一个叫做font的字体文件,这个文件加载后你就可以使用其中的文字内容。注意它虽然叫做font但是它不是原来windows上理解的这种字体。这种概念要有。

bascom生成这种文件有两种方式:

1、ide本身的font edit 

2、Bascom AVR Font Converter

这两种方式都可以,我推荐第二个。因为它可以生成不同宽度高度和块的font文件,并且它是一键生成的方式(购买后的情况下)。

格式说明:

A font file is a plain text file.

Lets have a look at the first few lines of the 8x8 font:


Font8x8:

$asm

.db 1,8,8,0

.db 0,0,0,0,0,0,0,0 ;

.db 0,0,6,95,6,0,0,0 ; !


The first line contains the name of the font. With the SETFONT statement you can select the font. Essential, this sets a data pointer to the location of the font data.

The second line ($ASM) is a directive for the internal assembler that asm code will follow.

All other lines are data lines.

The third line contains 4 bytes: 1 (height in bytes of the font) , 8 (width in pixels of the font), 8 (block size of the font) and a 0 which was not used before the 'truetype' support, but used for aligning the data in memory. This because AVR object code is a word long.

This last position is 0 by default. Except for 'TrueType' fonts. In BASCOM a TrueType font is a font where every character can have it's own width. The letter 'i' for example takes less space then the letter 'w'. The EADOG128 library demonstrates the TrueType option.

In order to display TT, the code need to determine the space at the left and right of the character. This space is then skipped and a fixed space is used between the characters. You can replace the 0 by the width you want to use. The value 2 seems a good one for small fonts.

All other lines are bytes that represent the character.

看不懂的话直接右键翻译,知道这些后你还是无法显示汉字,因为你不知道怎么去索引font的汉字位置,我也是翻来翻去看到的。打开ide的font edit。你会发现一个ascii。

屏幕快照 2019-10-06 下午4.12.45.png

可以看到有32和33,所以直接lcdat 1,1,chr(32)就可以索引到“王”这个汉字

整个代码

Cls
Setfont Font16x16
Lcdat 0,0,chr(32);
Setfont Font8x8tt 
Lcdat 8,16, "And" 
Setfont Font16x16
Lcdat 0,16+8+8+8-3,chr(33);
Setfont Font8x8tt 
Lcdat 8,16+8+8+8-3+16, "PCB"
Waitms 3500
Cls

批量制作汉字的话可以利用PCtoLcd,配置模式如下:

屏幕快照 2019-10-06 下午4.15.21.png

这是ssd1306芯片的取模方式的配置。其他的需要自己看手册。这样就会生成db代码。然后把db代码放到一个font文件里,载入到font edit 就可以看到效果。 如果遇到错误建议删除空行。