L ỜI NÓI ĐẦU
3.3.4. Lập trình GPIO trên RaspberryPi
Một trong những ứng dụng chủ yếu của Raspberry Pi là sử dụng trong các thiết bị điều khiển tự động. Để tiến hành điều khiển, đọc, nhận dữ liệu từ các
thiết bị bên ngoài chúng ta cần thao tác với các chân cắm mở rộng trên kit Raspberry Pi
Để sử dụng, điều khiển các chân cắm mở rộng GPIO trên kit Raspberry Pi một cách đơn giản và dễ hiểu người ta thường sử dụng bộ thư viện WiringPI. WiringPi là một thư viện truy cập GPIO viết bằng C cho BCM2837 sử dụng trong Raspberry Pi. Nó được phát hành theo giấy phép GNU LGPLv3 và là có thể sử dụng từ C và C ++ và nhiều ngôn ngữ khác. Với bộ thư viện này, chúng ta có thể lập trình vào ra cơ bản cho từng chân của Raspberry Pi, hoặc có thể cấu hình, tổ hợp cácchân để làm các tính năng chuyên biệt như : điều chế độ rộng xung PWM, giao tiếp UART, SPI, I2C… Và nhiều tính năng khác nữa.
Để sử dụng được bộ thư viện WirringPi cho lập trình GPIO việc đầu tiên là phải tiến hành cài đặt bộ thư viện này vào kít Raspberry Pi chạy hệ điều hành Raspbian. Các bước cài đặt này như sau:
Bước 1: Cài đặt chương trình download dữ liệu GIT. Mở terminal gõ lệnh:
pi@raspberrypi:~$sudo apt-get install git-core
Bước 2: Doawnload bộthư viện và các ví dụ về wiringPi về kit bằng cách gõ lệnh:
pi@raspberrypi:~$git clonegit://git.drogon.net/wiringPi
Sau khi download, thư viện wiringPi sẽ nằm trong thư mục<strong>/home/Pi.
Bước3: Chuyển đến thư mục wiringPi để tiến hành Build thư viện này.
pi@raspberrypi:~ $ cd wiringPi pi@raspberrypi:~$./build
Sau khi tiến hành cài đặt theo các bước trên bộthư viện wiringPi sẽđược cài đặt trên máy. Để kiểm tra xem quá trình cái đặt có thành cơng hay khơng ở cửa sổ Teminal gõ lệnh :
pi@raspberrypi:~$gpio–v
Sử dụng hai lệnh trên tại cửa số teminal sẽ hiển thị ra phiên bản wiringPi và cấu hình các chân hiện tại của kit Raspberry Pi
VD lập trình bật tắt LED: #include <wiringPi.h> int main(void) { wiringPiSetupGpio(); pinMode(17, OUTPUT); while(1){ digitalWrite(17, HIGH); } return 0; 3.3.5. Lập trình socket
3.3.5.1. Giới thiệu về mơ hình client/server
Là mơ hình phổ biến cho giao tiếp giữa 2 tiến trình/2 máy, trong đó, tiến trình client kết nối đến tiến trình server để yêu cầu trao đổi dữ liệu.
Client cần biết về sự tồn tại và địa chỉ của tiến trình server, nhưng server khơng cần biết về sự tồn tại và địa chỉ của client cho đến khi kết nối được thiết lập.
Mỗi khi kết nối được thiết lập, cả 2 bên đều có thể trao đổi (gửi và nhận dữ liệu).
Để thiết lập một kết nối cho cả 2 phía, cần xây dựng một socket. Có thể hiểu socket như một điểm đầu cuối của kênh kết nối giữa 2 tiến trình. (Giống như là thực hiện một kết nối giữa 2 người trong một cuộc gọi điện thoại)
Các hệ thống (linux, windows) đều cung cấp các hàm hệ thống để thực hiện thiết lập một socket.
3.3.5.2. Các loại socket
Có 2 loại socket được sử dụng rộng rãi là: stream sockets và datagram sockets:
Stream sockets: Dựa trên giao thức TCP (Tranmission Control Protocol), là giao thức hướng luồng (stream oriented). Việc truyền dữ liệu chỉ thực hiện giữa 2 tiến trìnhđã thiết lập kết nối. Giao thức này đảm bảo dữ liệu
được truyền đến nơi nhận một cách đáng tin cậy, đúng thứ tự nhờ vào cơ chế quản lý luồng lưu thông trên mạng và cơ chế chống tắc nghẽn.
Datagram sockets: Dựa trên giao thức UDP (User Datagram Protocol), là giao thức hướng thông điệp (message oriented). Việc truyền dữliệukhông yêu cầu có sự thiết lập kết nối giữa tiến quá trình. Ngược lại với giao thức TCP thì dữ liệu được truyền theo giao thức UDP khơng được tin cậy, có thế khơng đúng trình tự và lặp lại. Tuy nhiên vì nó khơng u cầu thiết lập kết nối khơng phải có những cơ chế phức tạpnên tốc độ nhanh…ứng dụng cho các ứng dụng truyền dữ liệu nhanh như chat, game,...
3.3.4.3. Thiết lập socket
Các bước thiết lập một socket phía client gồm: Tạo một socket bằng hàm socket()
Kết nối socket đến địa chỉ của server bằng hàm connect()
Gửi và nhận dữ liệu: Có một số cách khác nhau, đơn giản nhất là sử dụng các hàm read() và write()
Đóng kết nối bằng hàm close()
Các bước thiết lập một socket phía server gồm: Tạo một socket bằng hàm socket()
Đối với server trên internet địa chỉ bao gồm địa chỉ ip của máy host + số hiệu cổng dịch vụ (port number)
Lắng nghe (listen) các kết nối đến từ clients sử dụng hàm listen()
Chấp nhận các kết nối sử dụng hàm accept(). Hàm này sẽ dừng (block) cho đến khi nhận được một client kết nối đến.
Gửi và nhận dữ liệu với client (hàm read(), write())
Đóng kết nối bằng hàm close()
Kết luận chương
Chương 3 trình bày về tổng quan máy tính nhúng Raspberry Pi. Cấu trúc phần cứng, chức năng và bộ nhớ RAM … Ngồi ra cịn giới thiệu về Raspbian, hệ điều hành hoạt động trên Raspberry Pi 3. Các tập lệnh cơ bản trên Raspbian và tổng quan về lập trình socket.
CHƯƠNG 4
THIẾT KẾ CHƯƠNG TRÌNH TRUYỀN ÂM THANH QUA MẠNG 4G
4.1. Phân tích bài tốn 4.1.1. Mơ hình bài tốn
Dựa trên yêu cầu của đồ án truyền dữ liệu qua mạng 4G mà dữ liệu ở đây em dùng là âm thanh(audio). Để truyền nhận ta cần tạo chương trình gồm có 1 server và 1 client để trao đổi dữ liệu cho nhau, ở đây em dùng 1 PC làm server và Board Raspberry Pi làm client được kết nối theo hình 4.1
Hình 4.1: Mơ hình bài tốn Chức năng các thành phần trên: Chức năng các thành phần trên:
Mic/File Audio: được kết nối tới máy tính dùng để thu âm thanh bên ngồi mơi trường. File audio là file âm thanh có sẵn để phát
Máy tính: có nhiệm vụ đọc dữ liệu từ bộ đệm nhận, sau đó lưu vào bộ đêm truyền để truyền ra mạng Internet.
Modem: dùng để điều chế sóng tín hiệu tương tự nhau để mã hóa dữ liệu số.
Mạng Internet: dùng để truyền, nhận dữ liệu giữa các thiết bị với nhau. Modem 4G: dùng để điều chế sóng tín hiệu tương tự nhau để mã hóa dữ
Board nhúng Raspberry Pi: có nhiệm vụ nhận dữ liệu từ bên máy tính gửi đến thơng qua mạng Internet (4G). Sau đó phát âm thanh ra loa.
Trong mơ hình 3.1 thì chỉ có địa chỉ của modem và modem 4G là địa chỉ toàn cục định tuyến được còn địa chỉ của board Raspberry Pi và máy tính (PC) là các địa chỉ cục bộ khơng định tuyến được nên việc truyền dữ liệu giữa chúng gặp khó khăn, hơn nữa đa phần địa chỉ IP của các modem là địa chỉIP động nên thường xuyên thay đổi. Do đó để giải quyết 2 vấn đềnày ta làm như sau:
Để có thể biết được địa chỉ IP của modem ta sẽ sử dụng giải pháp dùng tên miền để thay thếđịa chỉ IP của modem.
Để có thể kết nối được tới máy tính thì ta phải cài đặt modem định hướng gói tin truyền đến máy tính.
4.1.2. Cài đặt tên miền cho modem
Do địa chỉ IP của các modem là địa chỉ IP động, thường xuyên thay đổi. Để có thể biết được địa chỉ IP của modem ta sẽ sử dụng giải pháp dùng tên miền để thay thế địa chỉ IP của modem. Ta có thể dùng 1 số trang web có thể tạo ra tên miền như: no-ip.com, dyndns.com vv…
Trên trang no-ip.com ta có thể tạo được tên miền miễn phí, sau đây là các bước để tạo 1 tên miền trên trang no-ip.com:
Bước 1: Vào trang https://www.noip.com. Nếu đã có tài khoản rồi thì ta có thể đăng nhập bằng cách click chuột vào sing in, nếu chưa có thì ta sẽ tạo 1 tài khoản mới bằng cách click sing up để có thể tạo 1 tài khoản.
Bước 2: Nhập thông tin tạo tài khoản
Email: phuongpham.god@gmail.com Password: xxxxxxxxxxxx
Hình 4.3: Nhập thơng tin để tạo tài khoản Bước 3: Ta đăng kí một Hostname
Hostname: tcpstream / ddns.net Record Type: chọn DNS Host(A)
IP Address: địa chỉ IP của modem (tự động cập nhật theo modem của ta)
Hình 4.4: Tham số Host
Sau khi điền xong các tham số ta nhấn Add Host và kết quả là ta đã tạo được tên miền là tcpstream.ddns.net
Hình 4.5: Quản lí Host
4.1.3. Cài đặt modem định hướng gói tin
Để board có thể kết nối đến máy tính (PC) thơng qua mạng Internet thì ta phải có cơ sở hạ tầng mạng Internet thì thực hiện các cài đặt modem để có thể truy cập tới PC-Server ảo.
Muốn kết nối được thì bên phía client phải biết địa chỉ của server, nhưng ở đây ta chỉ biết được địa chỉ của modem còn địa chỉ của server là địa chỉ cục bộ,
không định tuyến được. Vậy ta phải cấu hình cho modem sao cho khi ta truyền dữ liệu tới modem thì modem sẽ gửi tới server bằng cách tạo ra server ảo.
Để cấu hình cho modem tạo server ảo (ở đây em dùng modem TP-Link) ta làm các bước sau:
Bước 1: Vào địa chỉ của modem thường mặc định là http://192.168.1.1 và đăng nhập tên người dùng và mất khẩu của modem (thông thường theo mặc định thì tên người dùng và mất khẩu là: admin)
Hình 4.6: Đăng nhập vào modem Bước 2: Click vào Security Setup =>Port Forwarding Bước 2: Click vào Security Setup =>Port Forwarding
Bước 3: Mở Port
Application: Customer Settings Device: Manually Enter IP Address
IP Address: Địa chỉ IP LANcần mở port (192.168.1.153) TCP/UDP Port: Nhập port cần mở vào (9000)
Protocol: Chọn Both
Starting Port: Nhập port cần mở vào (9000) Ending Port: Nhập port cần mở vào (9000)
Bước 4: Sau khi click Add để cài thơng tin
Hình 4.9: Thơng tin Port đã mở
4.2. Thiết kế chương trình server trên máy tính4.2.1. Tổng quan 4.2.1. Tổng quan
Chương trình trên máy tính em dùng Visual Studio C# để thiết kế chương trình thu âm thanh (mở file audio) và truyền lên mạng Internet. Chương trình cũng gồm có 2 tiến trình chạy song song: 1 tiến trình dùng để thu âm thanh và 1 tiến trình dùng để truyền âm thanh đi.
Hình 4.10: Giao diện trên máy tính
4.2.2. Lập trình tạo Socket trên C#
Ta sẽ sử dụng các hàm mà C# đã hỗ trợ săn dùng cho việc tạo kết nối socket như:
IPAddress: Địa chỉ giao tiếp của một IP trên mạng.
Socket: là lớp cũng cấp nhiều phương thức và thuộc tính để giao tiếp mạng.
TcpListener: dùng để lắng nghe kết nối từ những máy trong mạng. Thiết lập miền địa chỉ IP và cổng để lắng nghe. Gọi phương thức Start() để có thể ắt đầu lắng nghe. AcceptSocket() là phương thức lắng nghe kết nối trả về. Khi đó đã kết nối được và có thể gửi và nhận dữ liệu.
Ví dụ: Đoạn chương trình mơ tả việc máy tính (có địa chỉ IP là x.x.x.x) nắng nghe kết nối tới máy khác (có cổng là Port_Number)
m_endpoint = new IPEndPoint(IPAddress.Parse(strIPAdress), Port); m_tcpip = new TcpListener(m_endpoint);
TcpClient client = m_tcpip.AcceptTcpClient();
4.2.3. Chương trình ghi dữ liệu từ mic
Lưu đồ thuật tốn chương trình ghi dữ liệu từ mic:
Ở đây ta sử dụng thư viện WinSound để thu âm thanh : private void StartRecordingFromSounddevice_Server() {
//Khởi tạo ghi âm
m_Recorder_Server = new WinSound.Recorder(); //Gọi sự kiện lấy dữ liệu và xử lí m_Recorder_Server.DataRecorded += new WinSound.Recorder.DelegateDataRecorded(OnDataReceivedFr omSoundcard_Server); m_Recorder_Server.Start(Config.SoundInputDeviceNameServe r, Config.SamplesPerSecondServer, Config.BitsPerSampleServer, Config.ChannelsServer, m_SoundBufferCount, bufferSize) m_JitterBufferServerRecording.Start(); }
Với xử lí âm thanh và đẩy vào buffer bằng hàmOnDataReceivedFromSoundcard_Server() :
private void OnDataReceivedFromSoundcard_Server(Byte[] data) { //Chia nhỏ dữ liệu int bytesPerInterval = WinSound.Utils.GetBytesPerInterval((uint) m_Config.SamplesPerSecondServer, m_Config.BitsPerSampleServer, m_Config.ChannelsServer); int count = data.Length / bytesPerInterval;
for (int i = 0; i < count; i++) {
Byte[] partBytes = new Byte[bytesPerInterval]; Array.Copy(data, currentPos, partBytes, 0, bytesPerInterval);
currentPos += bytesPerInterval;
WinSound.RTPPacket rtp = ToRTPPacket(partBytes, m_Config.BitsPerSampleServer,
m_Config.ChannelsServer);
// Đưa dữ liệu vào bộđệm JitterBuffer:
m_JitterBufferServerRecording.AddData(rtp); }
}
Khi tạo gói RTP, hầu hết các thơng tin như CSRC Count hoặc Version đều giống nhau. Sau mỗi gói RTP được gửi chỉ phải
tăng SequenceNumber và Timestamp. Trước đó, ta nén dữ liệu tuyến tính sang định dạng U-Law để giảm kích thước gói tin:
private WinSound.RTPPacket ToRTPPacket(Byte[] linearData, int bitsPerSample, int channels)
{
//Nén dữ liệu
Byte[] mulaws = WinSound.Utils.LinearToMulaw(linearData, bitsPerSample, channels);
//Tạo gói RTP
WinSound.RTPPacket rtp = new WinSound.RTPPacket(); rtp.Data = mulaws;
rtp.CSRCCount = m_CSRCCount; rtp.Extension = m_Extension; rtp.HeaderLength = WinSound.RTPPacket.MinHeaderLength; rtp.Marker = m_Marker; rtp.Padding = m_Padding; rtp.PayloadType = m_PayloadType; rtp.Version = m_Version; rtp.SourceId = m_SourceId;
//Tăng SequenceNumber và Timestamp sau mỗi gói tin try { rtp.SequenceNumber = Convert.ToUInt16(m_SequenceNumber); m_SequenceNumber++; } catch (Exception) { m_SequenceNumber = 0; } try { rtp.Timestamp = Convert.ToUInt32(m_TimeStamp); m_TimeStamp += mulaws.Length; } catch (Exception)
{
m_TimeStamp = 0; }
return rtp; }
Để gửi dữ liệu đi mà đảm bảo các gói tin gửi đi cách nhau một khoảng thời gian đều nhau, ta sử dụng thêm bộđệm JitterBuffer. Bộđệm JitterBuffer sẽđảm bảo các gói tin truyền đi với khoảng cách đều nhau, sau đây và tiến trình cài đặt bộđệm JitterBuffer và thêm sự kiện gửi gói tin:
m_JitterBufferServerRecording = new WinSound.JitterBuffer(null, RecordingJitterBufferCount, 20);
m_JitterBufferServerRecording.DataAvailable += new
WinSound.JitterBuffer.DelegateDataAvailable(OnJitterBufferServerD ataAvailable);
4.2.4. Chương trình phát âm thanh từ file audio
Lưu đồ thuật toán phát âm thanh từ file audio:
Để phát âm thanh từ file audio có sẵn, ta cần phải mở file audio :
private void ButtonOpenFileDialog_Click_1(object sender, EventArgs e) { if (OpenFileDialogMain.ShowDialog(this) == System.Windows.Forms.DialogResult.OK) { TextBoxFileName.Text = OpenFileDialogMain.FileName; } }
Đọc thông tin wav file từhàm WavFile trong thư viện WinSound : WinSound.WaveFileHeader header =
WinSound.WaveFile.Read(Config.FileName); Sau đó chia nhỏ dữ liệu :
m_RTPPartsLength =
WinSound.Utils.GetBytesPerInterval(header.SamplesPerSecond, header.BitsPerSample, header.Channels);
Chuyển đổi dữ liệu trước khi gửi :
m_PartByte = new Byte[m_RTPPartsLength];
Array.Copy(m_FilePayloadBuffer, m_CurrentRTPBufferPos, m_PartByte, 0, m_RTPPartsLength);
m_CurrentRTPBufferPos += m_RTPPartsLength; WinSound.RTPPacket rtp = ToRTPPacket(m_PartByte, m_FileHeader.BitsPerSample, m_FileHeader.Channels); Đưa dữ liệu vào bộ đệm JitterBuffer:
4.2.5. Gửi dữ liệu tới clients
Khi đã cài đặt bộ đệm JitterBuffer, JitterBuffer sẽ gọi tới hàm gửi sau mỗi 20ms:
private void OnJitterBufferServerDataAvailable(Object sender, WinSound.RTPPacket rtp)
{
Byte[] rtpBytes = rtp.ToBytes(); List<NF.ServerThread> list = new
List<NF.ServerThread>(m_Server.Clients); foreach (NF.ServerThread client in list) {
client.Send(rtpBytes); }
}
4.3. Thiết kế chương trình client trên board nhúng Raspberry Pi. 4.3.1. Tổng quan 4.3.1. Tổng quan
Để kết nối tới server để trao đổi dữ liệu ta dùng 1 socket để kết nối.
Client cần biết về sự tồn tại và địa chỉ của tiến trình server, nhưng server khơng cần biết về sự tồn tại và địa chỉ của client cho đến khi kết nối được thiết lập.
Mỗi khi kết nối được thiết lập, cả 2 bên đều có thể trao đổi (gửi và nhận dữ liệu).
Để thiết lập một kết nối cho cả 2 phía, cần xây dựng một socket. Có thể hiểu socket như một điểm đầu cuối của kênh kết nối giữa 2 tiến trình. Các hệ thống đều cung cấp các hàm hệ thống để thực hiện thiết lập một socket.
Trên board nhúng Raspberry, ta sử dụng ngơn ngữ lập trình C có thể biên dịch và chạy trên nền tảng HĐH Raspbian. Chương trình trên client có chức năng nhận dữ liệu từ mạng sau đó xử lý ra âm thanh và phát ra loa.
Giải thuật cho chương trình client:
Nhận dữ liệu theo gói RTP từ server thông qua socket Giải mã âm thanh
Phát âm thanh.
4.3.2. Lập trình socket trên client
Mơ hình bài tốn:
Hình 4.13: Mơ hình lập trình socket giữa client và server Các bước thiết lập một socket phía client gồm: Các bước thiết lập một socket phía client gồm:
Tạo một socket bằng hàm socket()
Kết nối socket đến địa chỉ của server bằng hàm connect()
Gửi và nhận dữ liệu: Có một số cách khác nhau, đơn giản nhất là sử dụng các hàm read() và write()
Các bước thiết lập một socket phía server gồm: Tạo một socket bằng hàm socket()
Gắn (bind) socket đến địa chỉ của server sử dụng hàm bind().
Đối với server trên internet địa chỉ bao gồm địa chỉ ip của máy host + số hiệu cổng dịch vụ (port number)
Lắng nghe (listen) các kết nối đến từ clients sử dụng hàm listen() Chấp nhận các kết nối sử dụng hàm accept(). Hàm này sẽ dừng
(block) cho đến khi nhận được một client kết nối đến. Gửi và nhận dữ liệu với client (hàm read(), write()) Đóng kếtnối bằng hàm close()
Chương trình khởi tạo socket trên client: void init_socket()
{
int portno=9000;
struct sockaddr_in serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0)
printf("ERROR opening socket");
memset(&serv_addr, '0', sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(portno); if(inet_pton(AF_INET,"118.71.4.45", &serv_addr.sin_addr)<=0) {
printf("\n inet_pton error occured\n"); }
if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\n Error : Connect Failed \n"); }
}
4.3.3. Lập trình đa luồng