較完整的串口類(WINAPI/C++/源碼),解決10以上端口,合理結束線程等問題

        串口在工業應用是極爲普遍的,我用API封裝了同步和異步的串口類,以及一個具有監視線程的異步串口類;使用簡單高效,具有工業強度,我在BC, BCB, VC, BCBX, GCC下編譯通過,相信足夠應付大多數情況,而且還可以繼承擴展,下面簡單介紹使用方法, 後附源代碼(_com.h);

        庫的層次結構:

   _base_com:虛基類,基本接口,可自行擴展自己的串口類
   _sync_com:_base_com 的子類, 同步應用,適合簡單應用
   _asyn_com:_base_com 的子類, 異步應用(重疊I/O),適合較高效應用,NT平臺
   _thread_com:_asyn_com 的子類, 異步應用,監視線程,適合較複雜應用,窗口通知消息和繼承擴展的使用方式;

   幾個問題:

     結束線程

    如何從WaitCommEvent(pcom->_com_handle, &mask, &pcom->_wait_o)這個API退出以便順利結束線程:
    方案1:
     SetCommMask(_com_handle, 0);    這個方法在MSDN有載,當在一些情況下並不完全有效,原因未知;
    方案2: 
     SetEvent(_wait_o.hEvent);    直接激活重疊IO結構中的事件句柄,絕對有效;  這份代碼我兩種都用;

    打開10以上的COM端口

   在NT/2000下打開編號10以上端口用
    _com_handle = CreateFile(
   “COM10“,
   GENERIC_READ | GENERIC_WRITE,
   0,
   NULL,
   OPEN_EXISTING,
   FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //重疊I/O
   NULL
   );

將提示錯誤, 這樣就OK:

_com_handle = CreateFile(
   “////.//COM10“,//對應的就是
//./COM10
   GENERIC_READ | GENERIC_WRITE,
   0,
   NULL,
   OPEN_EXISTING,
   FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //重疊I/O
   NULL
   );

    線程中循環的低效率問題

    使用SetCommMask(pcom->_com_handle, EV_RXCHAR | EV_ERR)監視接受字符和錯誤消息;一旦有個字符來就會激活WaitCommEvent 
通常作以下接受操作:

if(!WaitCommEvent(pcom->_com_handle, &mask, &pcom->_wait_o))
   {
    if(GetLastError() == ERROR_IO_PENDING)
    {
     GetOverlappedResult(pcom->_com_handle, &pcom->_wait_o, &length, true);
    }
   }

   if(mask & EV_ERR) // == EV_ERR
    ClearCommError(pcom->_com_handle, &error, &stat);

   if(mask & EV_RXCHAR) // == EV_RXCHAR
   {
       
 pcom->on_receive();//接收到字符
        //或發送到窗口消息
   }

這樣頻繁的函數調用或接受發送消息,效率低下,我添加掃描緩衝區的代碼,當字符數超過設定的字符數才作接受字符的操作;

if(mask & EV_RXCHAR) // == EV_RXCHAR
   {
    
ClearCommError(pcom->_com_handle, &error, &stat);
    if(stat.cbInQue > pcom->_notify_num) //
_notify_num 是設定得字符數
     pcom->on_receive();
   }

    類似於流的輸出方式

我編了一個簡單的寫串口的方式,可以類似於流將簡單的數據類型輸出

template<typename T>
 _asyn_com& operator << (T x)
 {
  strstream s;

  s << x ;
  write(s.str(), s.pcount());

  return *this;
 }

就可以這樣使用

_sync_com com1;
com1.open(1, 9600);
com1  << “ then random() 's return value is “<< rand()  << “ ./n“ ;
com1.close();

本串口類庫的主要接口

class _base_com
{
   bool open(int port);
   bool open(int port, int baud_rate);
   bool open(int port, char * set_str);
// set_str : “9600, 8, n, 1“
   bool set_state(int BaudRate, int ByteSize = 8, int Parity = NOPARITY, int StopBits = ONESTOPBIT)
   //設置內置結構串口參數:波特率,停止位
   bool set_state(char *set_str)
   bool is_open();
   HANDLE get_handle();
   virtual bool open_port()=0;
//繼承用的重要函數
  
virtual close();
}

class _sync_com :public _base_com //同步
{
    int read(char *buf, int buf_size);
//自動補上'/0',將用去一個字符的緩衝區
   
int write(char *buf, int len);
    int write(char *buf);
}



class _asyn_com  :public _base_com //異步

{
    int read(char *buf, int buf_size);
//自動補上'/0',將用去一個字符的緩衝區
   
int write(char *buf, int len);
    int write(char *buf);
}

class _thread_com  :public _asyn_com  //線程
{
    virtual void on_receive() //供線程接受到字符時調用, 可繼承替換之
    {
       if(_notify_hwnd)
            PostMessage(_notify_hwnd, ON_COM_RECEIVE, WPARAM(_port), LPARAM(0));
       else
       {
           if(_func)
              _func(_port);
       }
    }
    void set_hwnd(HWND hwnd); //設置窗口句柄, 發送 ON_COM_RECEIVE WM_USER + 618
    void set_func(void (*f)(int));
//設置調用函數 ,窗口句柄優先
   void set_notify_num(int num);
//設定發送通知, 接受字符最小值
}

一些應用範例 

   當然首先 #include
"_com.h"

    一、打開串口1同步寫

  char str[] = "com_class test";
 _sync_com com1; 
//同步
 com1.open(1); 
// 相當於 com1.open(1, 9600);  com1.open(1, "9600,8,n,1");
 for(int i=0; i<100; i++)
 {
  Sleep(500);
  com1.write(str); 
//也可以 com1.write(str, strlen(str));
 }
 com1.close();

    二、打開串口2異步讀

 char str[100];
 _asyn_com com2; //異步
 com2.open(2);
// 相當於 com2.open(2, 9600);  com2.open(2, "9600,8,n,1");
 if(!com2.is_open())
  cout << "COM2 not open , error : " << GetLastError() << endl;
 
/*
   也可以如下用法
  if(!com2.open(2))
   cout << "COM2 not open , error : " << GetLastError() << endl;
 */
 for(int i=0; i<100; i++)
 {
  Sleep(500);
  if(com2.read(str, 100) > 0) //異步讀,返回讀取字符數
   cout << str;
 }
 com2.close();

    三、擴展應用具有監視線程的串口類
     
 class _com_ex : public thread_com
 {
 public:
  virtual on_receive()
  {
   char str[100];
   if(read(str, 100) > 0) //異步讀,返回讀取字符數
     cout << str;
  }
 };

 int main(int argc, char *argv[])
 {
  try
  {
   char str[100];
   _com_ex com2; 
//異步擴展
   com2.open(2);
   Sleep(10000);
   com2.close();
  }
  catch(exception &e)
  {
   cout << e.what() << endl;
  }
  return 0;
 }

   
四、桌面應用可發送消息到指定窗口(在C++ Builder 和 VC ++ 測試通過)
  
   VC ++ 

   接受消息
  
 BEGIN_MESSAGE_MAP(ComDlg, CDialog)
  //{{AFX_MSG_MAP(ComDlg)
  ON_WM_SYSCOMMAND()
  ON_WM_PAINT()
  ON_WM_QUERYDRAGICON()
  ON_WM_DESTROY()
  //}}AFX_MSG_MAP
  
ON_MESSAGE(ON_COM_RECEIVE, On_Receive)
 END_MESSAGE_MAP()

   
打開串口,傳遞窗口句柄

   _thread_com com2;
  com2.open(2);
  com2.
set_hwnd(ComDlg->m_hWnd);

   處理消息
 
 LRESULT ComDlg::On_Receive(WPARAM wp, LPARAM lp)
 {
  char str[100];
  com2.read(str, 100);

  char com_str[10];
  strcpy(com_str, "COM");
  ltoa((long)wp, com_str + 3, 10);
// WPARAM 保存端口號

  MessageBox(str, com_str, MB_OK);
  return 0;
 }

  C++ Builder 
 
  class TForm1 : public TForm
 {
 __published: // IDE-managed Components
  void __fastcall FormClose(TObject *Sender, TCloseAction &Action);
  void __fastcall FormCreate(TObject *Sender);
 private: // User declarations
 public:  // User declarations

  
void On_Receive(TMessage& Message);

  __fastcall TForm1(TComponent* Owner);

  _thread_com com2;

  
BEGIN_MESSAGE_MAP
   MESSAGE_HANDLER(ON_COM_RECEIVE, TMessage, On_Receive)
  END_MESSAGE_MAP(TForm)
 };
 
 void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
 {
   com2.close();
 }

 //---------------------------------------------------------------------------
 void __fastcall TForm1::FormCreate(TObject *Sender)
 {
     com2.open(2);
     com2.set_hwnd(Handle);
 }

 //---------------------------------------------------------------------------
 void  TForm1::On_Receive(TMessage& Message)
 {
   char xx[20];
   int port = Message.WParam;
   if(com2.read(xx, 20) > 0)
     ShowMessage(xx);
 }
 
錯誤和缺陷在所難免,歡迎來信批評指正;
[email protected] 

附完整源代碼 _com.h


/*
串口基礎類庫(WIN32) ver 0.1

編譯器 : BC++ 5; C++ BUILDER 4, 5, 6, X; VC++ 5, 6; VC.NET;  GCC;

class   _base_com : 虛基類 基本串口接口;
class   _sync_com : 同步I/O 串口類;
class   _asyn_com : 異步I/O 串口類;
class _thread_com : 異步I/O 輔助讀監視線程 可轉發窗口消息 串口類(可繼承虛函數on_receive用於讀操作);
class        _com : _thread_com 同名

copyright(c) 2004.8 llbird [email protected]
*/
/*
Example :
*/
#ifndef _COM_H_
#define _COM_H_

#pragma warning(disable: 4530)
#pragma warning(disable: 4786)
#pragma warning(disable: 4800)

#include <cassert>
#include <strstream>
#include <algorithm>
#include <exception>
#include <iomanip>
using namespace std;

#include <windows.h>

class _base_com   //虛基類 基本串口接口
{
protected:

 volatile int _port;  //串口號
 volatile HANDLE _com_handle;//串口句柄
 char _com_str[20];
 DCB _dcb;     //波特率,停止位,等
 COMMTIMEOUTS _co;  // 超時時間

 virtual bool open_port() = 0;
 void init() //初始化
 {
  memset(_com_str, 0, 20);
  memset(&_co, 0, sizeof(_co));
  memset(&_dcb, 0, sizeof(_dcb));
  _dcb.DCBlength = sizeof(_dcb);
  _com_handle = INVALID_HANDLE_VALUE;
 }                 
 virtual bool setup_port()
 {
  if(!is_open())
   return false;

  if(!SetupComm(_com_handle, 8192, 8192))
   return false; //設置推薦緩衝區

  if(!GetCommTimeouts(_com_handle, &_co))
   return false;
  _co.ReadIntervalTimeout = 0xFFFFFFFF;
  _co.ReadTotalTimeoutMultiplier = 0;
  _co.ReadTotalTimeoutConstant = 0;
  _co.WriteTotalTimeoutMultiplier = 0;
  _co.WriteTotalTimeoutConstant = 2000;
  if(!SetCommTimeouts(_com_handle, &_co))
   return false; //設置超時時間

  if(!PurgeComm(_com_handle, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR ))
   return false; //清空串口緩衝區

  return true;
 }      
 inline void set_com_port(int port)
 {
  char p[12];
  _port = port;
  strcpy(_com_str, "
////.//COM"); 
  ltoa(_port, p, 10);
  strcat(_com_str, p);
 }
public:
 _base_com()
 {
  init(); 
 }
 virtual ~_base_com()
 {
  close();     
 }
 //設置串口參數:波特率,停止位,等 支持設置字符串 "9600, 8, n, 1"
 bool set_state(char *set_str) 
 {
  if(is_open())
  {
   if(!GetCommState(_com_handle, &_dcb))
    return false;
   if(!BuildCommDCB(set_str, &_dcb))
    return false;
   return SetCommState(_com_handle, &_dcb) == TRUE;
  }
  return false;
 }
 //設置內置結構串口參數:波特率,停止位
 bool set_state(int BaudRate, int ByteSize = 8, int Parity = NOPARITY, int StopBits = ONESTOPBIT)
 {
  if(is_open())
  {
   if(!GetCommState(_com_handle, &_dcb))
    return false;
   _dcb.BaudRate = BaudRate;
      _dcb.ByteSize = ByteSize;
      _dcb.Parity   = Parity;
   _dcb.StopBits = StopBits;
   return SetCommState(_com_handle, &_dcb) == TRUE;
  }
  return false;
 }
 //打開串口 缺省 9600, 8, n, 1
 inline bool open(int port)
 {
  return open(port, 9600);
 }
 //打開串口 缺省 baud_rate, 8, n, 1
 inline bool open(int port, int baud_rate)
 {
  if(port < 1 || port > 1024)
   return false;

  set_com_port(port);

  if(!open_port())
   return false;

  if(!setup_port())
   return false;

  return set_state(baud_rate);
 }
 //打開串口
 inline bool open(int port, char *set_str)
 {
  if(port < 1 || port > 1024)
   return false;

  set_com_port(port);

  if(!open_port())
   return false;

  if(!setup_port())
   return false;

  return set_state(set_str);
  
 }
 inline bool set_buf(int in, int out)
 {
  return is_open() ? SetupComm(_com_handle, in, out) : false;
 }
 //關閉串口
 inline virtual void close()
 {
  if(is_open())  
  {
   CloseHandle(_com_handle);
   _com_handle = INVALID_HANDLE_VALUE;
  }
 }
 //判斷串口是或打開
 inline bool is_open()
 {
  return _com_handle != INVALID_HANDLE_VALUE;
 }
 //獲得串口句炳
 HANDLE get_handle()
 {
  return _com_handle;
 }
 operator HANDLE()
 {
  return _com_handle;
 }
};

class _sync_com : public _base_com
{
protected:
 //打開串口
 virtual bool open_port()
 {
  if(is_open())
   close();

  _com_handle = CreateFile(
   _com_str,
   GENERIC_READ | GENERIC_WRITE,
   0,
   NULL,
   OPEN_EXISTING,
   FILE_ATTRIBUTE_NORMAL , 
   NULL
   );
  assert(is_open());
  return is_open();//檢測串口是否成功打開
 }

public:

 _sync_com()
 {
 }
 //同步讀
 int read(char *buf, int buf_len)
 {
  if(!is_open())
   return 0;

  buf[0] = '/0';
  
  COMSTAT  stat;
  DWORD error;

  if(ClearCommError(_com_handle, &error, &stat) && error > 0) //清除錯誤
  {
   PurgeComm(_com_handle, PURGE_RXABORT | PURGE_RXCLEAR); /*清除輸入緩衝區*/
   return 0;
  }
  
  unsigned long r_len = 0;

  buf_len = min(buf_len - 1, (int)stat.cbInQue);
  if(!ReadFile(_com_handle, buf, buf_len, &r_len, NULL))
    r_len = 0;
  buf[r_len] = '/0';

  return r_len;
 }
 //同步寫
 int write(char *buf, int buf_len)
 {
  if(!is_open() || !buf)
   return 0;
  
  DWORD    error;
  if(ClearCommError(_com_handle, &error, NULL) && error > 0) //清除錯誤
   PurgeComm(_com_handle, PURGE_TXABORT | PURGE_TXCLEAR);

  unsigned long w_len = 0;
  if(!WriteFile(_com_handle, buf, buf_len, &w_len, NULL))
   w_len = 0;

  return w_len;
 }
 //同步寫
 inline int write(char *buf)
 {
  assert(buf);
  return write(buf, strlen(buf));
 }
 //同步寫, 支持部分類型的流輸出
 template<typename T>
 _sync_com& operator << (T x)
 {
  strstream s;

  s << x;
  write(s.str(), s.pcount());

  return *this;
 }
};

class _asyn_com : public _base_com
{
protected:

 OVERLAPPED _ro, _wo; // 重疊I/O

 virtual bool open_port()
 {
  if(is_open())
   close();

  _com_handle = CreateFile(
   _com_str,
   GENERIC_READ | GENERIC_WRITE,
   0,
   NULL,
   OPEN_EXISTING,
   FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //重疊I/O
   NULL
   );
  assert(is_open());
  return is_open();//檢測串口是否成功打開
 }

public:

 _asyn_com()
 {
  memset(&_ro, 0, sizeof(_ro));
  memset(&_wo, 0, sizeof(_wo));

  _ro.hEvent = CreateEvent(NULL, true, false, NULL);
  assert(_ro.hEvent != INVALID_HANDLE_VALUE);
  
  _wo.hEvent = CreateEvent(NULL, true, false, NULL);
  assert(_wo.hEvent != INVALID_HANDLE_VALUE); 
 }
 virtual ~_asyn_com()
 {
  close();

  if(_ro.hEvent != INVALID_HANDLE_VALUE)
   CloseHandle(_ro.hEvent);

  if(_wo.hEvent != INVALID_HANDLE_VALUE)
   CloseHandle(_wo.hEvent);
 }
 //異步讀
 int read(char *buf, int buf_len, int time_wait = 20)
 {
  if(!is_open())
   return 0;

  buf[0] = '/0';

  COMSTAT  stat;
  DWORD error;

  if(ClearCommError(_com_handle, &error, &stat) && error > 0) //清除錯誤
  {
   PurgeComm(_com_handle, PURGE_RXABORT | PURGE_RXCLEAR); /*清除輸入緩衝區*/
   return 0;
  }

  if(!stat.cbInQue)// 緩衝區無數據
   return 0;

  unsigned long r_len = 0;

  buf_len = min((int)(buf_len - 1), (int)stat.cbInQue);

  if(!ReadFile(_com_handle, buf, buf_len, &r_len, &_ro)) //2000 下 ReadFile 始終返回 True
  {
   if(GetLastError() == ERROR_IO_PENDING) // 結束異步I/O
   {
    //WaitForSingleObject(_ro.hEvent, time_wait); //等待20ms
    if(!GetOverlappedResult(_com_handle, &_ro, &r_len, false))
    {
     if(GetLastError() != ERROR_IO_INCOMPLETE)//其他錯誤
       r_len = 0;
    }
   }
   else
    r_len = 0;
  }
   
  buf[r_len] = '/0';
  return r_len;
 }
 //異步寫
 int write(char *buf, int buf_len)
 {
  if(!is_open())
   return 0;
  
  DWORD    error;
  if(ClearCommError(_com_handle, &error, NULL) && error > 0) //清除錯誤
   PurgeComm(_com_handle, PURGE_TXABORT | PURGE_TXCLEAR); 

  unsigned long w_len = 0, o_len = 0;
  if(!WriteFile(_com_handle, buf, buf_len, &w_len, &_wo))
   if(GetLastError() != ERROR_IO_PENDING)
    w_len = 0;

  return w_len;
 }
 //異步寫
 inline int write(char *buf)
 {
  assert(buf);
  return write(buf, strlen(buf));
 }
 //異步寫, 支持部分類型的流輸出
 template<typename T>
 _asyn_com& operator << (T x)
 {
  strstream s;

  s << x ;
  write(s.str(), s.pcount());

  return *this;
 }
};

//當接受到數據送到窗口的消息
#define ON_COM_RECEIVE WM_USER + 618  //  WPARAM 端口號

class _thread_com : public _asyn_com
{
protected:
 volatile HANDLE _thread_handle; //輔助線程
 volatile HWND _notify_hwnd; // 通知窗口
 volatile long _notify_num;//接受多少字節(>_notify_num)發送通知消息
 volatile bool _run_flag; //線程運行循環標誌
 void (*_func)(int port);

 OVERLAPPED _wait_o; //WaitCommEvent use

 //線程收到消息自動調用, 如窗口句柄有效, 送出消息, 包含窗口編號
 virtual void on_receive()
 {
  if(_notify_hwnd)
   PostMessage(_notify_hwnd, ON_COM_RECEIVE, WPARAM(_port), LPARAM(0));
  else
  {
   if(_func)
    _func(_port);
  }
 }
 //打開串口,同時打開監視線程
 virtual bool open_port()
 {
  if(_asyn_com::open_port())
  {
   _run_flag = true;
   DWORD id;
   _thread_handle = CreateThread(NULL, 0, com_thread, this, 0, &id); //輔助線程
   assert(_thread_handle);
   if(!_thread_handle)
   {
    CloseHandle(_com_handle);
    _com_handle = INVALID_HANDLE_VALUE;
   }
   else
    return true;
  }
  return false;
 }

public:
 _thread_com()
 {
  _notify_num = 0;
  _notify_hwnd = NULL;
  _thread_handle = NULL;
  _func = NULL;

  memset(&_wait_o, 0, sizeof(_wait_o));
  _wait_o.hEvent = CreateEvent(NULL, true, false, NULL);
  assert(_wait_o.hEvent != INVALID_HANDLE_VALUE); 
 }
 ~_thread_com()
 {
  close();

  if(_wait_o.hEvent != INVALID_HANDLE_VALUE)
   CloseHandle(_wait_o.hEvent);
 }
 //設定發送通知, 接受字符最小值
 void set_notify_num(int num)
 {
  _notify_num = num;
 }
 int get_notify_num()
 {
  return _notify_num;
 }
 //送消息的窗口句柄
 inline void set_hwnd(HWND hWnd)
 {
  _notify_hwnd = hWnd;
 }
 inline HWND get_hwnd()
 {
  return _notify_hwnd;
 }
 inline void set_func(void (*f)(int))
 {
  _func = f;
 }
 //關閉線程及串口
 virtual void close()
 {
  if(is_open())  
  {
   _run_flag = false;
   SetCommMask(_com_handle, 0);
   SetEvent(_wait_o.hEvent);

   if(WaitForSingleObject(_thread_handle, 100) != WAIT_OBJECT_0)
    TerminateThread(_thread_handle, 0);

   CloseHandle(_com_handle);
   CloseHandle(_thread_handle);

   _thread_handle = NULL;
   _com_handle = INVALID_HANDLE_VALUE;
   ResetEvent(_wait_o.hEvent);
  }
 }
 /*輔助線程控制*/
 //獲得線程句柄
 HANDLE get_thread()
 {
  return _thread_handle;
 }
 //暫停監視線程
 bool suspend()
 {
  return _thread_handle != NULL ? SuspendThread(_thread_handle) != 0xFFFFFFFF : false;
 }
 //恢復監視線程
 bool resume()
 {
  return _thread_handle != NULL ? ResumeThread(_thread_handle) != 0xFFFFFFFF : false;
 }
 //重建監視線程
 bool restart()
 {
  if(_thread_handle) /*只有已有存在線程時*/
  {
   _run_flag = false;
   SetCommMask(_com_handle, 0);
   SetEvent(_wait_o.hEvent);

   if(WaitForSingleObject(_thread_handle, 100) != WAIT_OBJECT_0)
    TerminateThread(_thread_handle, 0);

   CloseHandle(_thread_handle);

   _run_flag = true;
   _thread_handle = NULL;

   DWORD id;
   _thread_handle = CreateThread(NULL, 0, com_thread, this, 0, &id);
   return (_thread_handle != NULL); //輔助線程
  }
  return false;
 }

private:
 //監視線程
 static DWORD WINAPI com_thread(LPVOID para)
 {
  _thread_com *pcom = (_thread_com *)para; 
  

        if(!SetCommMask(pcom->_com_handle, EV_RXCHAR | EV_ERR))
   return 0;

  COMSTAT  stat;
  DWORD error;

  for(DWORD length, mask = 0; pcom->_run_flag && pcom->is_open(); mask = 0)
  {
   if(!WaitCommEvent(pcom->_com_handle, &mask, &pcom->_wait_o))
   {
    if(GetLastError() == ERROR_IO_PENDING)
    {
     GetOverlappedResult(pcom->_com_handle, &pcom->_wait_o, &length, true);
    }
   }

   if(mask & EV_ERR) // == EV_ERR
    ClearCommError(pcom->_com_handle, &error, &stat);

   if(mask & EV_RXCHAR) // == EV_RXCHAR
   {
    ClearCommError(pcom->_com_handle, &error, &stat);
    if(stat.cbInQue > pcom->_notify_num)
     pcom->on_receive();
   }
        }

  return 0;
 }
 
};

typedef _thread_com _com; //名稱簡化

#endif //_COM_H_

發佈了30 篇原創文章 · 獲贊 5 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章